demo/bump v3:头图重新设计(右侧今日胎动卡+找找宝宝收集条),四种部位顶肚皮,脚趾再弱化

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 16:28:28 +00:00
parent cd76d36531
commit 83611d82c5
2 changed files with 174 additions and 97 deletions

View File

@@ -3,14 +3,18 @@
<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 回弹(欠阻尼弹簧,过冲时反鼓起来)+ 周围一起晃;
· 戳中小脚印 → 脚印「嗖」地缩回去并往远离手指的方向溜走,过一两秒在别处冒出来。
不再有水波纹。下面的卡片是首页截图(静态),只为给头图一个真实语境。
<title>宝宝在肚子里动 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「整块头图是妈妈的肚皮」v3。
头图纹理改为 JS 现场合成(canvas):肚皮底 belly_bg.jpg + LOGO + 倒计时 + 周数 + 进度条 + 右侧「今日胎动」玻璃卡。
右侧原来是胎儿插画,现在放胎动卡:宝宝每动一下,次数 +1,下面一行写「刚刚 · 小拳头顶了一下」;
卡片也画在纹理里,所以被顶到时会和字一起鼓起来变形。
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}
@@ -20,15 +24,19 @@
.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}
/* 头图下缘的「找找宝宝」收集条:和首页「记一下」同一套白色玻璃胶囊语言 */
.hunt{position:absolute;left:0;right:0;top:0;display:flex;justify-content:center;pointer-events:none;z-index:3}
.hunt .bar{margin-top:calc(min(100vw,480px) * 216 / 375);display:flex;align-items:center;gap:6px;padding:6px 7px 6px 13px;border-radius:999px;
background:rgba(255,255,255,.84);-webkit-backdrop-filter:blur(12px) saturate(160%);backdrop-filter:blur(12px) saturate(160%);
box-shadow:0 8px 20px rgba(180,80,50,.16),inset 0 0 0 1px rgba(255,255,255,.95);white-space:nowrap;
transition:transform .35s cubic-bezier(.3,1.6,.5,1)}
.hunt .bar.pop{transform:scale(1.06)}
.hunt .t{font-size:12px;font-weight:700;color:#C2563B;letter-spacing:.3px;margin-right:2px}
.hunt .c{display:flex;align-items:center;gap:3px;padding:4px 8px;border-radius:999px;font-size:11.5px;font-weight:600;
color:#D99A86;background:#FBEDE6;transition:all .3s}
.hunt .c i{width:6px;height:6px;border-radius:50%;background:#EBC3B4;transition:background .3s}
.hunt .c.on{color:#fff;background:linear-gradient(135deg,#FF9A72,#F06A45);box-shadow:0 3px 8px rgba(240,106,69,.35)}
.hunt .c.on i{background:#FFF3C4}
/* 录屏用的手指 */
.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%);
@@ -43,15 +51,17 @@
<canvas id="cv"></canvas>
<i class="finger" id="finger"></i>
</div>
<div class="hint"><span id="hint">宝宝在里面踢你呢,<b>戳一下小脚丫</b></span></div>
<div class="hunt"><div class="bar" id="hunt"><span class="t">找找宝宝</span>
<span class="c" data-k="foot"><i></i>小脚丫</span><span class="c" data-k="fist"><i></i>小拳头</span>
<span class="c" data-k="knee"><i></i>小膝盖</span><span class="c" data-k="butt"><i></i>小屁股</span></div></div>
<img class="below" src="below.jpg" alt="">
</div>
<script>
(function () {
const W = 375, H = 270; // 头图逻辑尺寸(与首页一致)
const W = 375, H = 270, SC = 3; // 头图逻辑尺寸(与首页一致),纹理按 3 倍画
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 hunt = document.getElementById('hunt'), finger = document.getElementById('finger');
const gl = cv.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: REC, antialias: true });
if (!gl) { hero.classList.add('nogl'); return; }
@@ -61,50 +71,42 @@ 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 F; // 鼓起处 x,y,角度,高度(px)
uniform vec4 K; // 形状 半径x,半径y,尖度(指数),脚趾显隐(0-1)
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);
// 五个脚趾,从大到小;蜷曲时往脚掌收、变矮
vec2 loc(vec2 p){vec2 d=p-F.xy; float c=cos(F.z),s=sin(F.z); return vec2(c*d.x+s*d.y,-s*d.x+c*d.y);}
// 脚趾:只在小脚丫时隐约露一点,圆乎乎、很淡
float toes(vec2 p){
if(K.w<.01||F.w<.01) return 0.;
vec2 d=loc(p)/1.2; d.x*=G.x; d.y/=1.+G.z;
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);
toe[0]=vec3(-6.5,-22.,5.2); toe[1]=vec3(.5,-24.5,4.2); toe[2]=vec3(6.,-23.,3.8); toe[3]=vec3(10.3,-20.,3.5); toe[4]=vec3(13.4,-15.8,3.2);
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));
vec3 t=toe[i]; float w=sin(Tm*7.+float(i)*1.3)*.5+.5;
float sd=length(d-t.xy-vec2(0.,(3.+1.5*w)*cu))-t.z;
ht=max(ht,smoothstep(5.,-5.,sd)*(.5-.15*cu*w));
}
return F.w*max(hs,ht);
return F.w*K.w*ht;
}
// 鼓起:椭圆的软帐篷,尖度 K.z 越大顶越尖(小脚丫尖、小屁股缓)
float tent(vec2 p){
if(F.w<.01) return 0.;
vec2 d=loc(p)/K.xy; d.y/=1.+G.z*.5;
float r=sqrt(dot(d,d)+.014);
float u=clamp((1.-r)/(1.-.118),0.,1.);
return F.w*2.3*pow(u,K.z);
}
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);}
float hgt(vec2 p){return tent(p)+toes(p)*.12+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;
@@ -115,9 +117,8 @@ void main(){
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-=(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.));
@@ -125,15 +126,12 @@ void main(){
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);
// 鼓起处往右下投一层淡影,让它「立」起来
vec2 d0=loc(p)/K.xy*68.-vec2(7.,11.); c*=1.-.10*exp(-dot(d0,d0)/(48.*48.))*smoothstep(1.,9.,F.w)*(1.-th)*m;
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.,.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.);
}`;
@@ -144,21 +142,79 @@ void main(){
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');
const uS = U('S'), uF = U('F'), uK = U('K'), uG = U('G'), uP = U('P'), uJ = U('J'), uT = U('Tm');
gl.uniform2f(uS, W, H);
/* ---------- 纹理:canvas 现场合成,胎动卡的数字和文案会变 ---------- */
const tc = document.createElement('canvas'); tc.width = W * SC; tc.height = H * SC;
const cx = tc.getContext('2d');
const SRC = { bg: 'belly_bg.jpg', top: 'top.png', pill: '../_hero/pill.png', week: '../_hero/week.png', prog: '../_hero/prog.png' };
const IMG = {}; let loaded = 0;
const tex = gl.createTexture(); let ready = false;
const img = new Image();
img.onload = () => {
const FONT = '-apple-system,"PingFang SC","Noto Sans SC",sans-serif';
const card = { n: 12, line: '宝宝醒着,正在活动', ago: '' };
function rr(x, y, w, h, r) { cx.beginPath(); cx.moveTo(x + r, y); cx.arcTo(x + w, y, x + w, y + h, r); cx.arcTo(x + w, y + h, x, y + h, r); cx.arcTo(x, y + h, x, y, r); cx.arcTo(x, y, x + w, y, r); cx.closePath(); }
function compose() {
cx.setTransform(SC, 0, 0, SC, 0, 0);
cx.drawImage(IMG.bg, 0, 0, W, H);
// 右侧原来是宝宝插画的位置:一大团很淡的暖光 + 几颗小光点,让肚皮不空
const gr = cx.createRadialGradient(292, 118, 6, 292, 118, 120);
gr.addColorStop(0, 'rgba(255,236,214,.42)'); gr.addColorStop(.55, 'rgba(255,226,200,.14)'); gr.addColorStop(1, 'rgba(255,226,200,0)');
cx.fillStyle = gr; cx.fillRect(150, 0, 225, 250);
[[226, 52, 1.6, .55], [352, 44, 1.2, .5], [214, 196, 1.3, .45], [362, 168, 1.8, .4], [246, 210, 1, .5]].forEach(([x, y, r, a]) => {
const g2 = cx.createRadialGradient(x, y, 0, x, y, r * 4); g2.addColorStop(0, 'rgba(255,255,255,' + a + ')'); g2.addColorStop(1, 'rgba(255,255,255,0)');
cx.fillStyle = g2; cx.beginPath(); cx.arc(x, y, r * 4, 0, 7); cx.fill();
});
cx.drawImage(IMG.top, 0, 0, W, 50);
cx.drawImage(IMG.pill, 14, 52, 159.69, 42);
cx.font = '900 13.5px ' + FONT; cx.textBaseline = 'middle';
cx.shadowColor = 'rgba(120,30,10,.28)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1;
cx.fillStyle = '#FFF3C4'; cx.fillText('67', 120.6, 73.5);
cx.shadowColor = 'transparent';
cx.drawImage(IMG.week, 14, 86, 110.9, 70);
cx.drawImage(IMG.prog, 14, 150, 192, 36);
// 「今日胎动」玻璃卡
const X = 232, Y = 62, CW = 128, CH = 118;
cx.save();
cx.shadowColor = 'rgba(150,55,30,.16)'; cx.shadowBlur = 18; cx.shadowOffsetY = 8;
rr(X, Y, CW, CH, 20); cx.fillStyle = 'rgba(255,255,255,.17)'; cx.fill();
cx.restore();
const hl = cx.createLinearGradient(X, Y, X, Y + CH); hl.addColorStop(0, 'rgba(255,255,255,.22)'); hl.addColorStop(1, 'rgba(255,255,255,.04)');
rr(X, Y, CW, CH, 20); cx.fillStyle = hl; cx.fill();
cx.lineWidth = 1; cx.strokeStyle = 'rgba(255,255,255,.55)'; rr(X + .5, Y + .5, CW - 1, CH - 1, 19.5); cx.stroke();
cx.shadowColor = 'rgba(120,30,10,.25)'; cx.shadowBlur = 4; cx.shadowOffsetY = 1;
cx.fillStyle = '#FFF3C4'; cx.beginPath(); cx.arc(X + 17, Y + 20, 3.2, 0, 7); cx.fill();
cx.fillStyle = 'rgba(255,255,255,.95)'; cx.font = '700 12px ' + FONT; cx.fillText('今日胎动', X + 26, Y + 20.5);
cx.font = '900 40px ' + FONT; cx.fillStyle = '#fff';
const ns = String(card.n); cx.fillText(ns, X + 14, Y + 55);
const nw = cx.measureText(ns).width;
cx.font = '700 13px ' + FONT; cx.fillText('次', X + 18 + nw, Y + 62);
cx.shadowColor = 'transparent';
cx.fillStyle = 'rgba(255,255,255,.28)'; cx.fillRect(X + 14, Y + 82, CW - 28, 1);
cx.shadowColor = 'rgba(120,30,10,.22)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1;
cx.font = '600 11px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.92)';
let ln = card.line; while (cx.measureText(ln).width > CW - 28 && ln.length > 2) ln = ln.slice(0, -1);
cx.fillText(ln, X + 14, Y + 99);
cx.shadowColor = 'transparent';
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';
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, tc);
}
function setCard(n, line) { if (n != null) card.n = n; if (line) card.line = line; if (ready) compose(); }
Object.keys(SRC).forEach(k => {
const im = new Image(); im.onload = () => {
IMG[k] = im;
if (++loaded === Object.keys(SRC).length) {
gl.bindTexture(gl.TEXTURE_2D, tex);
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; compose();
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => { compose(); window.__ready = true; });
}
};
im.src = SRC[k];
});
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 3), r = hero.getBoundingClientRect();
@@ -167,35 +223,48 @@ void main(){
}
resize(); addEventListener('resize', resize);
/* ---------- 物理:都是阻尼弹簧 ---------- */
/* ---------- 宝宝的各个部位:形状不同,动法不同 ---------- */
// rx/ry 椭圆半径,sharp 尖度,toe 脚趾显隐,amp 高度,dur 停留秒数,roll 是否慢慢拱着挪
const PARTS = {
foot: { name: '小脚丫', verb: '蹬了一下', rx: 64, ry: 72, sharp: 1.7, toe: 1, amp: 9.5, dur: [2.4, 3.4] },
fist: { name: '小拳头', verb: '顶了一下', rx: 46, ry: 46, sharp: 1.9, toe: 0, amp: 8, dur: [1.8, 2.6] },
knee: { name: '小膝盖', verb: '拱了一下', rx: 50, ry: 86, sharp: 1.6, toe: 0, amp: 8.5, dur: [2.2, 3] },
butt: { name: '小屁股', verb: '拱起来了', rx: 100, ry: 84, sharp: 1.25, toe: 0, amp: 7, dur: [3, 3.8], roll: true },
};
const ORDER = ['foot', 'fist', 'butt', 'knee'];
const found = {};
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 };
const ft = { x: 290, y: 120, ang: -.3, side: 1, part: PARTS.foot, 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;
let mode = 'wait', wait = .5, t = 0, seq = 0;
const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [160, 168]];
// 一半以上的概率专挑字和卡片顶(大字「30」、倒计时胶囊、进度条、slogan、胎动卡),字会被撑起来变形
const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [292, 112], [300, 84], [150, 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.random() < .7) { 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 = 220 + Math.random() * 130; y = 70 + Math.random() * 120; }
if (Math.hypot(x - ft.x, y - ft.y) > 80) return { x, y };
}
return { x: 300, y: 120 };
}
function appear() {
const key = ORDER[seq++ % ORDER.length], P = PARTS[key];
const s = pickSpot();
ft.key = key; ft.part = P;
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;
ft.rollDir = s.x > 200 ? -1 : 1;
ft.h.to = P.amp; ft.h.k = P.roll ? 70 : 140; ft.h.c = P.roll ? 9 : 9; ft.h.vel += P.roll ? 30 : 120;
ft.life = 0; ft.dur = P.dur[0] + Math.random() * (P.dur[1] - P.dur[0]); ft.kicked = false;
mode = 'show';
setCard(card.n + 1, '刚刚' + P.name + P.verb);
}
function hide(fast) {
ft.h.to = 0;
@@ -206,17 +275,19 @@ void main(){
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++;
const fx = ft.x + ft.ox, fy = ft.y + ft.oy, R = Math.max(ft.part.rx, ft.part.ry) * .8;
if ((mode === 'show' || (mode === 'hide' && ft.h.v > 3)) && Math.hypot(x - fx, y - fy) < R) {
// 摁中:被摁回去,往远离手指的方向溜
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; } // 正中脚心:往脚跟方向缩
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);
setCard(null, ft.part.name + '被摁回去啦');
found[ft.key] = true;
const chip = hunt.querySelector('[data-k="' + ft.key + '"]'); if (chip) chip.classList.add('on');
hunt.classList.add('pop'); setTimeout(() => hunt.classList.remove('pop'), 300);
if (Object.keys(found).length === ORDER.length) hunt.querySelector('.t').textContent = '全找到啦!';
}
} else if (pk.held) {
pk.held = false;
@@ -238,15 +309,20 @@ void main(){
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; }
const u = ft.life, P = ft.part;
if (P.roll) {
// 小屁股:慢慢拱着往一边挪
ft.ox = ft.rollDir * 30 * Math.min(u / ft.dur, 1); ft.oy = -6 * Math.sin(Math.min(u / ft.dur, 1) * Math.PI);
ft.wob = Math.sin(u * 1.2) * .08; ft.curl = 0; ft.stretch = .05 * Math.sin(u * 1.6);
} else {
ft.curl = P.toe ? Math.max(0, Math.sin(u * 3.2 - .6)) * .9 : 0;
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);
} else if (mode === 'hide') {
if (ft.h.v < .3) { mode = 'wait'; wait = .9 + Math.random() * 1.2; ft.kicked = false; }
if (ft.h.v < .3) { mode = 'wait'; wait = .9 + Math.random() * 1.2; }
} else if (mode === 'flee') {
const f = ft.flee; f.t += dt;
const s = 26 * (1 - Math.exp(-f.t * 7));
@@ -254,7 +330,7 @@ void main(){
ft.stretch = .35 * Math.exp(-f.t * 5);
ft.curl = 1;
wait -= dt;
if (wait <= 0) { ft.kicked = false; appear(); }
if (wait <= 0) 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; }
@@ -262,8 +338,9 @@ void main(){
}
function draw() {
if (!ready) return;
const wob = mode === 'show' ? (ft.wob || 0) : 0;
const P = ft.part, 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(uK, P.rx, P.ry, P.sharp, P.toe);
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);
@@ -276,7 +353,7 @@ void main(){
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.__foot = () => ({ x: ft.x + ft.ox, y: ft.y + ft.oy, h: ft.h.v, mode, key: ft.key });
window.__finger = (x, y, on) => {
finger.style.left = x / W * 100 + '%'; finger.style.top = y / H * 100 + '%';
finger.classList.toggle('on', !!on);

BIN
demo/bump/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB