Files
mama-box/js/baby-limbs.js
2026-10-12 00:51:22 +00:00

178 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ================= 首页头图:宝宝时不时动一动小手小脚(2026-10-05) =================
用户:「右上角羊水袋里的小宝宝时不时稍微动一动小腿、小脚、小胳膊,缓缓地动,不要太激烈」。
⚠️ 之前试过 AI 生成的 GIF,整张一起扭,脸型、腿型都变(见 hero-fx.js show() 里的注释),所以这里不用动图:
在静图上盖一张 WebGL 画布,同一张 PNG 原样画上去,只在「关节 → 手脚末端」这一小段做小角度旋转扭动,
脸周围是保护圈(权重强制为 0),头和身子一像素都不动。
· 每个阶段的关节位置手工标在 LIMBS 里(坐标 = 480×480 原图像素)。1、2 阶段是胚芽,没有手脚,不动。
· 节奏(10-12 起只在孕 20 周起):每隔 4~9 秒随机挑一处(小腿 / 小脚 / 小胳膊),用 1.8~2.8 秒缓缓抬起再放回;
偶尔小腿和小脚一起「蹬」一下。角度只有 3~9 度。不动的时候不重画(不占电)。
· 跟 hero-fx.js 解耦:不改它,只盯 #heroStill 的 src / class —— 换阶段、淡入淡出都自动跟上。
· 没 WebGL / 系统「减少动态效果」→ 什么都不做,静图照常。
· 调试:URL 带 limbdebug=1 → 角度 ×4、动作更频繁,并把会动的区域染成蓝色,方便核对关节标得准不准。 */
(function () {
'use strict';
const still = document.getElementById('heroStill');
const box = document.getElementById('heroBaby');
if (!still || !box || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const DEBUG = /[?&]limbdebug=1/.test(location.search);
// 每处:[关节 x, y, 末端 x, y, 影响半径, 最大角度(度), 类型] 类型 leg / foot / arm(foot 会被 leg 带着一起蹬)
// face:[圆心 x, y, 半径] —— 圈内完全不动,1.5 倍半径处才恢复
const LIMBS = {
'450581c2f05f': { face: [228, 222, 24], l: [[268, 272, 300, 286, 14, 6, 'leg'], [242, 258, 266, 240, 9, 5, 'arm']] },
'dc4c7eea1c50': { face: [240, 207, 30], l: [[288, 272, 322, 320, 18, 5, 'leg'], [306, 318, 326, 310, 10, 9, 'foot'], [228, 272, 256, 236, 10, 3, 'arm']] },
'7bf792b04123': { face: [217, 205, 32], l: [[252, 262, 292, 302, 15, 6, 'leg'], [278, 305, 296, 298, 10, 10, 'foot'], [197, 283, 236, 244, 12, 5, 'arm']] },
'f140f4367b48': { face: [220, 208, 33], l: [[262, 262, 306, 303, 15, 6, 'leg'], [288, 305, 308, 300, 10, 10, 'foot'], [200, 282, 236, 244, 12, 5, 'arm']] },
'51e99b3060e2': { face: [222, 205, 34], l: [[276, 266, 335, 310, 17, 6, 'leg'], [322, 313, 338, 305, 11, 10, 'foot'], [232, 268, 278, 240, 12, 5, 'arm']] },
'a5dbb24c1ef2': { face: [225, 215, 40], l: [[276, 252, 322, 296, 16, 5, 'leg'], [310, 305, 326, 293, 11, 10, 'foot']] },
'610c4d92c16d': { face: [250, 205, 42], l: [[292, 272, 335, 305, 16, 4, 'leg'], [325, 312, 338, 300, 12, 9, 'foot']] }
};
const N = 4; // shader 里最多 4 处
const cv = document.createElement('canvas');
cv.className = 'hero-kid hero-limb';
cv.setAttribute('aria-hidden', 'true');
still.after(cv);
const gl = cv.getContext('webgl', { premultipliedAlpha: true, alpha: true }) || cv.getContext('experimental-webgl');
if (!gl) { cv.remove(); return; }
const VS = 'attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}';
const FS = [
'precision mediump float;',
'uniform vec2 uRes;uniform sampler2D uTex;uniform vec4 uL[' + N + '];uniform vec2 uR[' + N + '];uniform vec3 uF;uniform float uDbg;',
'void main(){',
' vec2 q=vec2(gl_FragCoord.x/uRes.x,1.-gl_FragCoord.y/uRes.y)*480.;',
' vec2 off=vec2(0.);float wsum=0.;',
' for(int i=0;i<' + N + ';i++){',
' float ang=uR[i].y; if(ang==0.) continue;',
' vec2 p=uL[i].xy,d=uL[i].zw-p; float s=dot(q-p,d)/dot(d,d);',
' float dist=length(q-(p+d*clamp(s,0.,1.))); float r=uR[i].x;',
// 关节处 0 → 段长一半处 1(关节不撕开);过了末端再渐隐;离开肢体 r 以外渐隐
' float w=smoothstep(.05,.55,s)*(1.-smoothstep(1.25,1.7,s))*(1.-smoothstep(r*.6,r*1.25,dist));',
' float a=-ang*w; vec2 v=q-p;',
' off+=vec2(cos(a)*v.x-sin(a)*v.y,sin(a)*v.x+cos(a)*v.y)-v; wsum+=w;',
' }',
' float fw=smoothstep(uF.z,uF.z*1.5,length(q-uF.xy));', // 脸保护圈
' vec4 c=texture2D(uTex,(q+off*fw)/480.);',
' if(uDbg>.5) c.rgb=mix(c.rgb,vec3(.2,.5,1.)*c.a,min(wsum*fw,1.)*.6);',
' gl_FragColor=c;',
'}'
].join('\n');
function sh(t, src) { const s = gl.createShader(t); gl.shaderSource(s, src); gl.compileShader(s); return s; }
const pr = gl.createProgram();
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS)); gl.linkProgram(pr);
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) { cv.remove(); return; }
gl.useProgram(pr);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(pr, 'a');
gl.enableVertexAttribArray(aLoc); gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
const U = {}; ['uRes', 'uTex', 'uL', 'uR', 'uF', 'uDbg'].forEach(k => { U[k] = gl.getUniformLocation(pr, k); });
gl.uniform1i(U.uTex, 0); gl.uniform1f(U.uDbg, DEBUG && !/limbdebug=2/.test(location.search) ? 1 : 0); // limbdebug=2:放大角度但不染色
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
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);
/* 2026-10-12 用户:「20 周以后的宝宝,偶尔让他稍微动一动,微微动一动脚和手,幅度不要太大」。
原来 11 周起都动、角度 3~9°,实测 10 秒里只有两三百个像素在变,基本看不出来。
改成:只有孕 20 周起才动;手 3°→5°、小腿 5°→6°、小脚 9°→10°(仍是「微微」),间隔放宽到 4~9 秒(「偶尔」)。
孕周来自 hero-fx.js 同一个 mb-preg 事件(没建档 / 产后不动);kidweek=NN 调试参数照样认。 */
const FORCE = (location.search.match(/[?&]kidweek=(\d+)/) || [])[1];
let week = FORCE != null ? +FORCE : (window.__mbPreg && !window.__mbPreg.postpartum ? window.__mbPreg.week : null);
addEventListener('mb-preg', e => { if (FORCE == null) { const d = e.detail || {}; week = d.postpartum ? null : d.week; } });
const old = () => week >= 20;
let cfg = null, ready = false, raf = 0, timer = 0, seen = true, lastPick = -1;
const moves = []; // 正在进行的动作:{ i, t0, dur, amp }
const home = document.getElementById('screen-home');
const active = () => seen && !document.hidden && (!home || home.classList.contains('is-active'));
function size() {
const d = Math.min(3, devicePixelRatio || 1), w = Math.round(Math.min(720, box.clientWidth * d)) || 480;
if (cv.width !== w) { cv.width = cv.height = w; gl.viewport(0, 0, w, w); }
gl.uniform2f(U.uRes, w, w);
}
function draw(now) {
const L = new Float32Array(N * 4), R = new Float32Array(N * 2);
let busy = false;
for (let k = moves.length - 1; k >= 0; k--) {
const m = moves[k], u = (now - m.t0) / m.dur;
if (u >= 1) { moves.splice(k, 1); continue; }
if (u < 0) { busy = true; continue; }
busy = true;
const e = Math.sin(Math.PI * u); R[m.i * 2 + 1] += m.amp * e * e; // 缓起缓落:0 → 最大 → 0
}
cfg.l.forEach((l, i) => { if (i < N) { L.set(l.slice(0, 4), i * 4); R[i * 2] = l[4]; } });
gl.uniform4fv(U.uL, L); gl.uniform2fv(U.uR, R); gl.uniform3fv(U.uF, cfg.face);
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
return busy;
}
function loop(now) {
raf = 0;
if (!ready || !active()) return;
if (draw(now)) raf = requestAnimationFrame(loop);
}
const kick = () => { if (!raf && ready) raf = requestAnimationFrame(loop); };
// 安排下一次「动一动」
function plan() {
clearTimeout(timer);
timer = setTimeout(() => {
if (ready && active() && cfg.l.length && (old() || DEBUG)) {
let i = Math.floor(Math.random() * cfg.l.length);
if (i === lastPick && cfg.l.length > 1) i = (i + 1) % cfg.l.length;
lastPick = i;
const now = performance.now(), l = cfg.l[i], dir = Math.random() < .5 ? -1 : 1;
const dur = 1800 + Math.random() * 1000, deg = l[5] * (.6 + Math.random() * .4) * (DEBUG ? 4 : 1);
moves.push({ i, t0: now, dur, amp: dir * deg * Math.PI / 180 });
// 小腿动时 4 成概率小脚也跟着轻轻翘一下(像蹬了一下),稍晚 0.3 秒起
const f = cfg.l.findIndex(x => x[6] === 'foot');
if (l[6] === 'leg' && f >= 0 && Math.random() < .4)
moves.push({ i: f, t0: now + 300, dur: dur - 200, amp: -dir * cfg.l[f][5] * .7 * (DEBUG ? 4 : 1) * Math.PI / 180 });
kick();
}
plan();
}, DEBUG ? 900 + Math.random() * 600 : 4000 + Math.random() * 5000);
}
// 跟着 #heroStill:换图时重新载贴图,静图淡出时画布一起淡出
function sync() {
const m = (still.getAttribute('src') || '').match(/([0-9a-f]{12})\.png/);
const next = m && LIMBS[m[1]];
if (!next) { cfg = null; ready = false; cv.classList.remove('is-on'); return; }
if (next !== cfg) {
cfg = next; ready = false; moves.length = 0;
cv.classList.remove('is-on');
const img = new Image();
img.onload = () => {
if (cfg !== next) return;
size();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
ready = true; draw(performance.now());
if (still.classList.contains('is-on')) cv.classList.add('is-on');
};
img.src = still.src;
return;
}
if (ready) cv.classList.toggle('is-on', still.classList.contains('is-on'));
}
// 调试:__limbPose(k) 把所有手脚定格在 k × 最大角度(k=0 原图),方便截图核对
if (DEBUG) window.__limbPose = k => {
clearTimeout(timer); moves.length = 0;
cfg.l.forEach((l, i) => moves.push({ i, t0: performance.now() - 500, dur: 1000, amp: k * l[5] * Math.PI / 180 }));
draw(performance.now()); moves.length = 0;
};
new MutationObserver(sync).observe(still, { attributes: true, attributeFilter: ['src', 'class'] });
sync();
plan();
addEventListener('resize', () => { if (ready) { size(); draw(performance.now()); } });
if ('IntersectionObserver' in window) new IntersectionObserver(es => { seen = es[0].isIntersecting; }).observe(box);
cv.addEventListener('webglcontextlost', () => { ready = false; cv.classList.remove('is-on'); });
})();