298 lines
15 KiB
HTML
298 lines
15 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
|
||
<title>宝宝在肚子里踢你 · 创意预览</title>
|
||
<!-- 2026-10-03 创意预览(不进正式首页):「整块头图是妈妈的肚皮」v2。
|
||
头图去掉了胎儿插画(belly.jpg:原头图渐变拟合 + 一团圆鼓鼓的肚皮柔光,再叠回 LOGO/倒计时/周数/进度条)。
|
||
WebGL 把它当一层肉肉的肚皮:
|
||
· 宝宝时不时往外蹬一脚,把肚皮连同上面的字撑成一个软软的小帐篷(顶上隐约一只圆乎乎的小脚丫),扭一扭再缩回去,隔会儿换个地方,常常专挑字踢;
|
||
· 手指戳上去 = 肚皮被按下去一个窝,松手后像肉一样 duang duang 回弹(欠阻尼弹簧,过冲时反鼓起来)+ 周围一起晃;
|
||
· 戳中小脚印 → 脚印「嗖」地缩回去并往远离手指的方向溜走,过一两秒在别处冒出来。
|
||
不再有水波纹。下面的卡片是首页截图(静态),只为给头图一个真实语境。
|
||
?rec=1:录屏模式,由 __adv / __tap / __press 逐帧驱动(生成动图用)。 -->
|
||
<style>
|
||
html,body{margin:0;background:#F8F3EF;-webkit-tap-highlight-color:transparent;font-family:-apple-system,"PingFang SC","Noto Sans SC",sans-serif}
|
||
.page{position:relative;max-width:480px;margin:0 auto;overflow:hidden}
|
||
.hero{position:relative;width:100%;aspect-ratio:375/270;touch-action:manipulation;cursor:pointer}
|
||
.hero canvas,.hero img{position:absolute;inset:0;width:100%;height:100%;display:block}
|
||
.hero img{opacity:0} /* WebGL 起不来时退回静态头图 */
|
||
.hero.nogl img{opacity:1}
|
||
.below{display:block;width:100%;margin-top:0}
|
||
/* 头图下方的小提示:和首页「记一下」同一套白色胶囊语言 */
|
||
.hint{position:absolute;left:0;right:0;top:0;display:flex;justify-content:center;pointer-events:none;z-index:3}
|
||
.hint span{margin-top:calc(min(100vw,480px) * 222 / 375);padding:6px 14px 6px 12px;border-radius:999px;
|
||
font-size:12.5px;color:#C2563B;letter-spacing:.3px;font-weight:600;
|
||
background:rgba(255,255,255,.82);-webkit-backdrop-filter:blur(12px) saturate(160%);backdrop-filter:blur(12px) saturate(160%);
|
||
box-shadow:0 6px 16px rgba(180,80,50,.16),inset 0 0 0 1px rgba(255,255,255,.9);transition:transform .35s cubic-bezier(.3,1.6,.5,1)}
|
||
.hint span b{color:#F06A45}
|
||
.hint span.pop{transform:scale(1.08)}
|
||
.hint span{white-space:nowrap}
|
||
/* 录屏用的手指 */
|
||
.finger{position:absolute;width:38px;height:38px;margin:-19px 0 0 -19px;border-radius:50%;pointer-events:none;z-index:5;
|
||
background:radial-gradient(circle,rgba(255,255,255,.85) 0 30%,rgba(255,255,255,.35) 55%,rgba(255,255,255,0) 72%);
|
||
box-shadow:0 0 0 2px rgba(255,255,255,.7);opacity:0;transform:scale(1.25);transition:opacity .15s,transform .15s}
|
||
.finger.on{opacity:1;transform:scale(1)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page">
|
||
<div class="hero" id="hero">
|
||
<img src="belly.jpg" alt="">
|
||
<canvas id="cv"></canvas>
|
||
<i class="finger" id="finger"></i>
|
||
</div>
|
||
<div class="hint"><span id="hint">宝宝在里面踢你呢,<b>戳一下小脚丫</b></span></div>
|
||
<img class="below" src="below.jpg" alt="">
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
const W = 375, H = 270; // 头图逻辑尺寸(与首页一致)
|
||
const REC = /[?&]rec=1/.test(location.search);
|
||
const hero = document.getElementById('hero'), cv = document.getElementById('cv');
|
||
const hint = document.getElementById('hint'), finger = document.getElementById('finger');
|
||
const gl = cv.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: REC, antialias: true });
|
||
if (!gl) { hero.classList.add('nogl'); return; }
|
||
|
||
const VS = 'attribute vec2 a;varying vec2 v;void main(){v=a*.5+.5;v.y=1.-v.y;gl_Position=vec4(a,0.,1.);}';
|
||
const FS = `
|
||
precision highp float;
|
||
varying vec2 v;
|
||
uniform sampler2D T;
|
||
uniform vec2 S; // 逻辑尺寸 375x270
|
||
uniform vec4 F; // 脚印 x,y,角度,高度(px)
|
||
uniform vec4 G; // 脚印 左右脚(±1),脚趾蜷曲(0-1),拉长量,0
|
||
uniform vec4 P; // 手指 x,y,按压深度(负=凹下去,正=回弹鼓起),0
|
||
uniform vec2 J; // 肚皮整体晃动的位移(px)
|
||
uniform float Tm;
|
||
float sdE(vec2 p,vec2 c,vec2 r){return (length((p-c)/r)-1.)*min(r.x,r.y);}
|
||
float smin(float a,float b,float k){float h=clamp(.5+.5*(b-a)/k,0.,1.);return mix(b,a,h)-k*h*(1.-h);}
|
||
// 小脚丫:脚尖朝 -y,长约 52px
|
||
float foot(vec2 p){
|
||
if(F.w<.01) return 0.;
|
||
vec2 d=p-F.xy; float c=cos(F.z),s=sin(F.z);
|
||
d=vec2(c*d.x+s*d.y,-s*d.x+c*d.y);
|
||
d/=1.2; d.x*=G.x; d.y/=1.+G.z;
|
||
float sole=sdE(d,vec2(1.,-6.),vec2(11.5,11.));
|
||
sole=smin(sole,sdE(d,vec2(-1.5,6.),vec2(8.,11.)),10.);
|
||
sole=smin(sole,sdE(d,vec2(-1.,15.),vec2(8.5,9.)),10.);
|
||
float hs=smoothstep(8.,-14.,sole);
|
||
// 五个脚趾,从大到小;蜷曲时往脚掌收、变矮
|
||
float cu=G.y, ht=0.;
|
||
vec3 toe[5];
|
||
toe[0]=vec3(-6.5,-22.,4.8); toe[1]=vec3(.5,-24.5,3.7); toe[2]=vec3(6.,-23.,3.3); toe[3]=vec3(10.3,-20.,3.); toe[4]=vec3(13.4,-15.8,2.7);
|
||
for(int i=0;i<5;i++){
|
||
vec3 t=toe[i]; float w=sin(Tm*9.+float(i)*1.3)*.5+.5;
|
||
vec2 tc=t.xy+vec2(0.,(3.5+1.5*w)*cu);
|
||
float sd=length(d-tc)-t.z;
|
||
ht=max(ht,smoothstep(4.,-5.,sd)*(.62-.18*cu*w));
|
||
}
|
||
return F.w*max(hs,ht);
|
||
}
|
||
float dentR=33.;
|
||
float dent(vec2 p){vec2 d=p-P.xy;return P.z*exp(-dot(d,d)/(dentR*dentR));}
|
||
// 小帐篷:脚丫往外蹬,把肚皮连同上面的字一起撑成一个尖顶软帐篷
|
||
float tent(vec2 p){
|
||
if(F.w<.01) return 0.;
|
||
vec2 d=p-F.xy; float c=cos(F.z),s=sin(F.z);
|
||
d=vec2(c*d.x+s*d.y,-s*d.x+c*d.y); d.y*=.85;
|
||
float u=clamp(1.-sqrt(dot(d,d)+64.)/68.,0.,1.)/(1.-8./68.); // 顶上磨圆,不出尖折痕
|
||
return F.w*2.3*pow(u,1.7);
|
||
}
|
||
float hgt(vec2 p){return tent(p)+foot(p)*.3+dent(p);}
|
||
void main(){
|
||
vec2 p=v*S;
|
||
vec3 c0=texture2D(T,v).rgb;
|
||
// 只在橙色肚皮上生效:下缘弧线外是页面底色
|
||
// 白字也接近页面底色,所以只在下缘弧线那一带(y>185)按颜色判断
|
||
float m=p.y<185.?1.:smoothstep(.03,.12,distance(c0,vec3(.973,.953,.937)));
|
||
float e=1.2;
|
||
vec2 g=vec2(hgt(p+vec2(e,0.))-hgt(p-vec2(e,0.)),hgt(p+vec2(0.,e))-hgt(p-vec2(0.,e)))/(2.*e)*m;
|
||
float h=hgt(p)*m;
|
||
// 肉感:按下去时周围的皮肤被拽向手指,回弹时被推开;整块肚皮再跟着晃
|
||
vec2 dp=p-P.xy;
|
||
float gp=exp(-dot(dp,dp)/(dentR*dentR*2.6));
|
||
float gw=exp(-dot(dp,dp)/(130.*130.));
|
||
vec2 q=p+dp*(-P.z*.03)*gp - J*gw + g*2.2;
|
||
float fh=clamp(foot(p)/12.,0.,1.);
|
||
float th=clamp(tent(p)/16.,0.,1.2);
|
||
q-=(p-F.xy)*.36*th; // 帐篷顶把字往外撑开、放大:越靠近顶越被撑大
|
||
q=mix(p,q,m);
|
||
vec3 c=texture2D(T,clamp(q/S,0.,1.)).rgb;
|
||
vec3 n=normalize(vec3(-g*.85,1.));
|
||
vec3 L=normalize(vec3(-.45,-.75,.9));
|
||
float dif=dot(n,L)-dot(vec3(0.,0.,1.),L);
|
||
c+=vec3(1.,.95,.9)*max(dif,0.)*.30;
|
||
c*=1.+min(dif,0.)*vec3(.28,.36,.38); // 背光侧偏暖红,像皮肤
|
||
// 脚印四周一圈很淡的凹陷(皮被撑起,边上被拉紧)
|
||
float rim=smoothstep(.0,.25,fh)*(1.-smoothstep(.25,.7,fh));
|
||
// 按下去的窝:窝底暗一点、偏红一点(像压着肉)
|
||
// 帐篷往右下投一层淡影,让它「立」起来
|
||
vec2 d0=p-F.xy-vec2(7.,11.); c*=1.-.10*exp(-dot(d0,d0)/(48.*48.))*smoothstep(1.,9.,F.w)*(1.-th);
|
||
float dn=clamp(-dent(p)/10.,0.,1.)*m;
|
||
c*=1.-dn*vec3(.13,.21,.2);
|
||
vec3 Hh=normalize(L+vec3(0.,0.,1.));
|
||
c+=vec3(1.,.97,.95)*pow(max(dot(n,Hh),0.),24.)*.12*smoothstep(.5,4.,abs(h)); // 皮肤油润的柔高光
|
||
c+=vec3(1.,.88,.8)*smoothstep(2.,12.,h)*.05; // 顶起处微微透亮
|
||
gl_FragColor=vec4(c,1.);
|
||
}`;
|
||
function sh(t, s) { const o = gl.createShader(t); gl.shaderSource(o, s); gl.compileShader(o); if (!gl.getShaderParameter(o, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(o); return o; }
|
||
const pr = gl.createProgram();
|
||
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS)); gl.linkProgram(pr); gl.useProgram(pr);
|
||
const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
||
const al = gl.getAttribLocation(pr, 'a'); gl.enableVertexAttribArray(al); gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0);
|
||
const U = n => gl.getUniformLocation(pr, n);
|
||
const uS = U('S'), uF = U('F'), uG = U('G'), uP = U('P'), uJ = U('J'), uT = U('Tm');
|
||
gl.uniform2f(uS, W, H);
|
||
|
||
const tex = gl.createTexture(); let ready = false;
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||
ready = true; window.__ready = true;
|
||
};
|
||
img.src = 'belly.jpg';
|
||
|
||
function resize() {
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 3), r = hero.getBoundingClientRect();
|
||
cv.width = Math.round(r.width * dpr); cv.height = Math.round(r.height * dpr);
|
||
gl.viewport(0, 0, cv.width, cv.height);
|
||
}
|
||
resize(); addEventListener('resize', resize);
|
||
|
||
/* ---------- 物理:都是阻尼弹簧 ---------- */
|
||
const spring = (o, dt) => { const a = -o.k * (o.v - o.to) - o.c * o.vel; o.vel += a * dt; o.v += o.vel * dt; };
|
||
// 脚印:高度一根弹簧(蹬出来时带一点过冲),位置/角度跟着状态机走
|
||
const ft = { x: 290, y: 120, ang: -.3, side: 1, h: { v: 0, vel: 0, to: 0, k: 140, c: 11 }, curl: 0, stretch: 0, life: 0, dur: 3, ox: 0, oy: 0 };
|
||
// 手指按压:欠阻尼,松手后过冲成一个小鼓包再回来 → duang duang
|
||
const pk = { x: 0, y: 0, d: { v: 0, vel: 0, to: 0, k: 520, c: 6.5 }, held: false };
|
||
// 肚皮整体晃:二维弹簧,阻尼更小、频率更低
|
||
const jg = { x: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 }, y: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 } };
|
||
let mode = 'wait', wait = .5, t = 0, count = 0;
|
||
|
||
const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [160, 168]];
|
||
function pickSpot() {
|
||
for (let i = 0; i < 40; i++) {
|
||
// 一半概率专挑字踢(大字「30」、倒计时胶囊、进度条、slogan),字会被撑起来变形;一半落在右边空白肚皮
|
||
let x, y;
|
||
if (Math.random() < .55) { const s = TEXT_SPOTS[Math.floor(Math.random() * TEXT_SPOTS.length)]; x = s[0] + (Math.random() - .5) * 16; y = s[1] + (Math.random() - .5) * 10; }
|
||
else { x = 240 + Math.random() * 100; y = 80 + Math.random() * 100; }
|
||
if (Math.hypot(x - ft.x, y - ft.y) > 80) return { x, y };
|
||
}
|
||
return { x: 300, y: 120 };
|
||
}
|
||
function appear() {
|
||
const s = pickSpot();
|
||
ft.x = s.x; ft.y = s.y; ft.ox = ft.oy = 0;
|
||
ft.ang = (Math.random() - .5) * 1.4 + (s.x > 190 ? .25 : -.25);
|
||
ft.side = Math.random() < .5 ? 1 : -1;
|
||
ft.h.to = 9.5; ft.h.k = 140; ft.h.c = 9; ft.h.vel += 120; // 蹬一下冒出来
|
||
ft.life = 0; ft.dur = 2.6 + Math.random() * 1.4;
|
||
mode = 'show';
|
||
}
|
||
function hide(fast) {
|
||
ft.h.to = 0;
|
||
if (fast) { ft.h.k = 700; ft.h.c = 45; } else { ft.h.k = 60; ft.h.c = 14; }
|
||
mode = 'hide';
|
||
}
|
||
|
||
function press(x, y, on) {
|
||
if (on) {
|
||
pk.x = x; pk.y = y; pk.held = true; pk.d.to = -14; pk.d.k = 380; pk.d.c = 22;
|
||
const fx = ft.x + ft.ox, fy = ft.y + ft.oy;
|
||
if ((mode === 'show' || (mode === 'hide' && ft.h.v > 3)) && Math.hypot(x - fx, y - fy) < 58) {
|
||
// 戳中小脚丫:嗖地缩回去,往远离手指的方向溜
|
||
count++;
|
||
let dx = fx - x, dy = fy - y, l = Math.hypot(dx, dy);
|
||
if (l < 6) { dx = -Math.sin(ft.ang); dy = Math.cos(ft.ang); l = 1; } // 正中脚心:往脚跟方向缩
|
||
ft.flee = { dx: dx / l, dy: dy / l, t: 0 };
|
||
hide(true); mode = 'flee'; wait = 1.1 + Math.random() * .8;
|
||
if (navigator.vibrate) navigator.vibrate(15);
|
||
hint.innerHTML = count < 2 ? '被你戳到啦!小脚丫缩回去了' : '又溜啦~已经戳到宝宝 <b>' + count + '</b> 次';
|
||
hint.classList.add('pop'); setTimeout(() => hint.classList.remove('pop'), 300);
|
||
}
|
||
} else if (pk.held) {
|
||
pk.held = false;
|
||
// 松手:弹簧变软、阻尼变小,皮肉弹回来并过冲 → duang duang
|
||
pk.d.to = 0; pk.d.k = 360; pk.d.c = 4.6; pk.d.vel += 170;
|
||
jg.y.vel += 150; jg.x.vel += (Math.random() - .5) * 90;
|
||
}
|
||
}
|
||
hero.addEventListener('pointerdown', e => {
|
||
const r = hero.getBoundingClientRect();
|
||
hero.setPointerCapture && hero.setPointerCapture(e.pointerId);
|
||
press((e.clientX - r.left) / r.width * W, (e.clientY - r.top) / r.height * H, true);
|
||
});
|
||
const up = () => press(0, 0, false);
|
||
hero.addEventListener('pointerup', up); hero.addEventListener('pointercancel', up);
|
||
|
||
function step(dt) {
|
||
t += dt;
|
||
if (mode === 'wait') { wait -= dt; if (wait <= 0) appear(); }
|
||
else if (mode === 'show') {
|
||
ft.life += dt;
|
||
const u = ft.life;
|
||
// 扭一扭:轻轻转、往脚尖方向蹭一点、脚趾蜷一蜷;中途再蹬一下
|
||
ft.curl = Math.max(0, Math.sin(u * 3.2 - .6)) * .9;
|
||
ft.stretch = .06 * Math.sin(u * 2.4);
|
||
ft.wob = Math.sin(u * 2.1) * .12;
|
||
if (u > ft.dur * .45 && !ft.kicked) { ft.kicked = true; ft.h.vel += 70; }
|
||
if (u > ft.dur) { hide(false); ft.kicked = false; }
|
||
} else if (mode === 'hide') {
|
||
if (ft.h.v < .3) { mode = 'wait'; wait = .9 + Math.random() * 1.2; ft.kicked = false; }
|
||
} else if (mode === 'flee') {
|
||
const f = ft.flee; f.t += dt;
|
||
const s = 26 * (1 - Math.exp(-f.t * 7));
|
||
ft.ox = f.dx * s; ft.oy = f.dy * s;
|
||
ft.stretch = .35 * Math.exp(-f.t * 5);
|
||
ft.curl = 1;
|
||
wait -= dt;
|
||
if (wait <= 0) { ft.kicked = false; appear(); }
|
||
}
|
||
if (mode !== 'show' && mode !== 'flee') { ft.stretch *= Math.exp(-dt * 6); }
|
||
spring(ft.h, dt); if (ft.h.v < 0) { ft.h.v = 0; ft.h.vel *= -.2; }
|
||
spring(pk.d, dt); spring(jg.x, dt); spring(jg.y, dt);
|
||
}
|
||
function draw() {
|
||
if (!ready) return;
|
||
const wob = mode === 'show' ? (ft.wob || 0) : 0;
|
||
gl.uniform4f(uF, ft.x + ft.ox, ft.y + ft.oy, ft.ang + wob, ft.h.v);
|
||
gl.uniform4f(uG, ft.side, ft.curl, ft.stretch, 0);
|
||
gl.uniform4f(uP, pk.x, pk.y, pk.d.v, 0);
|
||
gl.uniform2f(uJ, jg.x.v, jg.y.v);
|
||
gl.uniform1f(uT, t);
|
||
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||
}
|
||
|
||
if (REC) {
|
||
// 录屏:由外部按帧推进
|
||
window.__adv = ms => { const n = Math.round(ms / (1000 / 120)); for (let i = 0; i < n; i++) step(1 / 120); draw(); };
|
||
window.__press = (x, y, on) => press(x, y, on);
|
||
window.__tap = (x, y) => press(x, y, true);
|
||
window.__foot = () => ({ x: ft.x + ft.ox, y: ft.y + ft.oy, h: ft.h.v, mode });
|
||
window.__finger = (x, y, on) => {
|
||
finger.style.left = x / W * 100 + '%'; finger.style.top = y / H * 100 + '%';
|
||
finger.classList.toggle('on', !!on);
|
||
};
|
||
window.__seed = s => { let z = s; Math.random = () => ((z = (z * 16807) % 2147483647) / 2147483647); };
|
||
} else {
|
||
let last = performance.now();
|
||
(function loop(now) {
|
||
const dt = Math.min((now - last) / 1000, 1 / 20); last = now;
|
||
const n = Math.max(1, Math.round(dt * 120));
|
||
for (let i = 0; i < n; i++) step(dt / n);
|
||
draw(); requestAnimationFrame(loop);
|
||
})(last);
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|