Files
mama-box/js/hero-fx.js

204 lines
13 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-04) =================
用户:「宝宝按孕周换、孕囊跟着宝宝大小变;顶部往下落的花瓣去掉,换成阳光和水波纹」。
① 换宝宝:孕周来自 app.js 的 mb-preg 事件(syncPregnancy 跑完广播,首屏一次、远程档案回来再一次)。
9 个阶段各一张「羊水囊 + 宝宝」整幅图:静图(首帧)先出,GIF 加载完再盖上淡入。
没建档 / 产后 / 事件迟迟不来 → 用第 6 阶段(原来那个 25 周的宝宝)。
孕囊大小 = .hero-baby 的 --k(相对原 25 周囊的倍数),CSS 里围绕同一圆心缩放、带过渡。
调试:URL 带 kidweek=12 强制按 12 周显示。
② 阳光水波:#heroFx 一张画布,screen 混合叠在宝宝上、文字下。
柔光束 + 柔和水波光影 + 悬浮微粒,宝宝区域避开(详见下面那段)。
不在首页 / 页面隐藏 / 头图滚出视口时停画;系统「减少动态效果」下整块不画(CSS 也隐藏)。 */
(function () {
'use strict';
// [起始孕周, 静图, GIF(null = 只用静图), 孕囊倍数 k, 宝宝轮廓遮罩(水波/光挖掉宝宝用)] 2026-10-04:宝宝在囊里缩到原图的 70%,孕囊不变
const STAGES = [
[0, 'd953571ad48d.jpg', null, .42, '91f1472ee58a.png'], // 1 4–7 周 胚芽 + 卵黄囊
[8, 'dd756c516072.jpg', null, .52, '5b39ca21a42e.png'], // 2 8–10 周 小豆芽胚胎
[11, 'a6f52893ad1e.jpg', null, .62, '0dcddb83b038.png'], // 3 11–13 周 刚有人形
[14, '251ec778dc15.jpg', null, .72, 'd63b80896e8e.png'], // 4 14–17 周 身子拉长、吮手
[18, 'dcf2ae48e0ec.jpg', null, .86, '0022b97f8cc0.png'], // 5 18–23 周 比例接近新生儿、偏瘦
[24, 'b0e24b10aaee.jpg', null, 1, 'd75b2a87e805.png'], // 6 24–27 周 原来那个宝宝
[28, '85da5aaebb4f.jpg', null, 1.01, '507e24e95ef0.png'], // 7 28–31 周 大一圈、有胎发
[32, '0780b4698253.jpg', null, 1.02, '681b0bdb885e.png'], // 8 32–35 周 肉嘟嘟、抱腿
[36, 'ff112c222b1e.jpg', null, 1.03, '494179795c67.png'] // 9 36 周起 足月、撑满羊水囊(头朝上:10-04 用户说头朝下看着不舒服)
];
const DEFAULT = 5;
const IMG = 'assets/img/';
const box = document.getElementById('heroBaby');
const still = document.getElementById('heroStill');
const kid = document.getElementById('heroKid');
if (!box || !still || !kid) return;
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const force = (location.search.match(/[?&]kidweek=(\d+)/) || [])[1];
function stageOf(week) {
if (week == null || isNaN(week)) return DEFAULT;
let i = 0;
while (i + 1 < STAGES.length && week >= STAGES[i + 1][0]) i++;
return i;
}
let cur = -1, seq = 0, babyMask = null; // babyMask:当前阶段宝宝轮廓(黑 + alpha),见 frame() 末尾
function show(i) {
if (i === cur) return;
cur = i;
const [, png, gif, k, mk] = STAGES[i], my = ++seq;
const mi = new Image(); mi.onload = () => { if (my === seq) babyMask = mi; }; mi.src = IMG + mk;
box.style.setProperty('--k', k);
// 先把旧图淡掉再换,避免新旧宝宝叠在一起闪一下
kid.classList.remove('is-on'); still.classList.remove('is-on');
const s = new Image();
s.onload = () => { if (my !== seq) return; still.src = s.src; still.classList.add('is-on'); };
s.src = IMG + png;
/* 2026-10-04 用户:「水波纹一出来,宝宝的脸型、腿型就变了」—— 那是 GIF 自带的整体扭动(生成动图的通病),
不是页面特效。先只用静图:宝宝一像素都不变形。gif 位传 null;以后有了只动手脚/嘴角的新动图再填回来。 */
if (reduce || !gif) return;
const g = new Image();
g.onload = () => { if (my !== seq) return; kid.src = g.src; kid.classList.add('is-on'); };
g.src = IMG + gif;
}
function onPreg(d) { show(stageOf(force != null ? +force : (d && d.week))); }
addEventListener('mb-preg', e => onPreg(e.detail));
if (force != null || window.__mbPreg) onPreg(window.__mbPreg);
else setTimeout(() => { if (cur < 0) show(DEFAULT); }, 2500);
/* ---------------- 阳光 + 水波 ----------------
2026-10-04 三版(用户:「光束太硬,要柔柔地射进来;水波纹可以有,但宝宝的脸不能变;不要白色网状水波纹」):
· 光束:画在 1/6 分辨率的小画布上再放大,边缘天然糊开 → 柔光;宽、淡、慢慢摆、忽明忽暗
· 水波:极低分辨率算几层正弦叠出的大片明暗,放大后是柔和的、缓缓流动的光影(不是网)
· 宝宝区域(孕囊中心 70% 半径内)水波压到很淡、光束减半;不再给孕囊挂任何形变滤镜,宝宝的脸一像素都不动
· 悬浮微粒:羊水里的小颗粒,慢慢漂、闪一下 */
const cv = document.getElementById('heroFx');
if (!cv || reduce) return;
const hero = cv.parentNode, home = document.getElementById('screen-home');
const ctx = cv.getContext('2d');
const RS = 6; // 光束小画布缩小倍数
const rc = document.createElement('canvas'), rx = rc.getContext('2d');
const CW = 44; // 水波低分辨率宽(越低越柔)
const off = document.createElement('canvas'), oc = off.getContext('2d');
let tick = 0, W = 0, H = 0, dpr = 1, ch = 0, img = null, seen = true, raf = 0, last = 0, prev = 0;
let SX = 0, SY = 0, SR = 1; // 孕囊圆心/半径(CSS px)
function size() {
const r = hero.getBoundingClientRect(), b = box.getBoundingClientRect();
dpr = Math.min(devicePixelRatio || 1, 2);
W = r.width; H = r.height;
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
rc.width = Math.max(8, Math.round(W / RS)); rc.height = Math.max(8, Math.round(H / RS));
ch = Math.max(8, Math.round(CW * H / W));
off.width = CW; off.height = ch; img = oc.createImageData(CW, ch);
SX = b.left - r.left + b.width / 2; SY = b.top - r.top + b.height / 2; SR = Math.max(b.width, 60) / 2;
}
// 宝宝保护:孕囊中心附近 → 0,囊边 → 1
function guard(x, y) {
const d = Math.hypot(x - SX, y - SY) / SR;
return d < .62 ? 0 : d > 1.05 ? 1 : (d - .62) / .43; // 宝宝缩到 70% 后大约占孕囊半径的 6 成
}
// 水波:四层不同方向、不同速度的长波叠加,取正半部分 → 一片片柔和的亮斑缓缓流过
function water(t) {
/* 2026-10-04 四版(用户:「整个上半部分要有羊水那种动态的感觉,现在还是偏静态」):
明暗两相 —— 波峰处暖白亮斑、波谷处淡淡的暗影,source-over 叠,看起来是光透过流动的水在晃;
尺度比上一版小一倍多(一屏能看到好几片),流速加快。仍是极低分辨率放大 → 边缘全是软的,不是网。 */
const d = img.data, T = t * .9;
for (let y = 0; y < ch; y++) {
for (let x = 0; x < CW; x++) {
const u = x / CW * 11, v = y / CW * 11;
let w = Math.sin(u * 1.1 + T) + Math.sin(v * 1.3 - T * .8 + u * .5) + Math.sin((u - v) * .9 + T * .7)
+ Math.sin(Math.hypot(u - 8, v - 2) * 1.4 - T * 1.2) * 1.2;
w /= 4.2;
const o = (y * CW + x) * 4, fade = 1 - y / ch * .3;
if (w > 0) { const k = Math.min(1, w * 1.6); d[o] = 255; d[o + 1] = 240; d[o + 2] = 218; d[o + 3] = k * k * 150 * fade; }
else { const k = Math.min(1, -w * 1.6); d[o] = 150; d[o + 1] = 60; d[o + 2] = 40; d[o + 3] = k * k * 55 * fade; }
}
}
oc.putImageData(img, 0, 0);
}
function drawWater() {
ctx.globalCompositeOperation = 'source-over';
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
ctx.drawImage(off, 0, 0, W, H);
}
// 柔光束:光源在头图上方偏右画外;每道是一条很宽的渐变扇形,画在小画布上再放大
const RAYS = [[-.12, .07, .8], [0, .05, .7], [.11, .08, 1], [.23, .06, .7]];
function rays(t) {
const w = rc.width, h = rc.height, ox = w * .8, oy = -h * .9, L = h * 2.8;
rx.setTransform(1, 0, 0, 1, 0, 0);
rx.clearRect(0, 0, w, h);
rx.globalCompositeOperation = 'lighter';
RAYS.forEach(([a0, ww, al], i) => {
// 2026-10-04 用户:「射下来的光束可以动一动」→ 摆幅 .03→.07、摆速加快,明暗起伏更明显
const a = Math.PI * .6 + a0 + Math.sin(t * .16 + i * 1.9) * .07;
const k = al * (.45 + .55 * Math.sin(t * .35 + i * 2.1));
const wv = ww * (1 + .2 * Math.sin(t * .17 + i));
const gr = rx.createLinearGradient(ox, oy, ox + Math.cos(a) * L * .8, oy + Math.sin(a) * L * .8);
gr.addColorStop(0, 'rgba(255,248,228,0)');
gr.addColorStop(.35, 'rgba(255,246,224,' + (.34 * k).toFixed(3) + ')');
gr.addColorStop(.65, 'rgba(255,240,215,' + (.15 * k).toFixed(3) + ')');
gr.addColorStop(1, 'rgba(255,235,205,0)');
rx.fillStyle = gr;
rx.beginPath(); rx.moveTo(ox, oy);
rx.lineTo(ox + Math.cos(a - wv) * L, oy + Math.sin(a - wv) * L);
rx.lineTo(ox + Math.cos(a + wv) * L, oy + Math.sin(a + wv) * L);
rx.closePath(); rx.fill();
});
// 宝宝区域光束减半:在小画布上挖一个软边的「淡区」
rx.globalCompositeOperation = 'destination-out';
const sx = SX / RS, sy = SY / RS, sr = SR / RS, g = rx.createRadialGradient(sx, sy, 0, sx, sy, sr * .9);
g.addColorStop(0, 'rgba(0,0,0,.5)'); g.addColorStop(.6, 'rgba(0,0,0,.35)'); g.addColorStop(1, 'rgba(0,0,0,0)');
rx.fillStyle = g; rx.beginPath(); rx.arc(sx, sy, sr, 0, 6.2832); rx.fill();
ctx.globalCompositeOperation = 'lighter';
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
ctx.drawImage(rc, 0, 0, W, H);
}
// 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;孕囊中心附近不画,免得落在宝宝脸上
const DOTS = Array.from({ length: 26 }, () => ({ x: Math.random(), y: Math.random(), r: .5 + Math.random() * 1.2,
vx: (Math.random() - .5) * .006, vy: -(.004 + Math.random() * .008), ph: Math.random() * 6.3, sp: .6 + Math.random() * 1.4 }));
function dots(t, dt) {
ctx.globalCompositeOperation = 'lighter';
for (const p of DOTS) {
p.x += (p.vx + Math.sin(t * .5 + p.ph) * .002) * dt; p.y += p.vy * dt;
if (p.y < -.03) { p.y = 1.03; p.x = Math.random(); }
if (p.x < -.03) p.x = 1.03; else if (p.x > 1.03) p.x = -.03;
const X = p.x * W, Y = p.y * H, gd = guard(X, Y);
if (gd <= 0 && !babyMask) continue;
const al = (.15 + .25 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * (babyMask ? 1 : gd);
const R = p.r * 2.4, g = ctx.createRadialGradient(X, Y, 0, X, Y, R);
g.addColorStop(0, 'rgba(255,250,238,' + al.toFixed(3) + ')');
g.addColorStop(1, 'rgba(255,250,238,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(X, Y, R, 0, 6.2832); ctx.fill();
}
}
function frame(now) {
raf = 0;
if (!(seen && !document.hidden && (!home || home.classList.contains('is-active')))) { prev = 0; return; }
raf = requestAnimationFrame(frame);
if (now - last < 33) return; // ≈30fps 足够,省电
const dt = prev ? Math.min((now - prev) / 1000, .1) : 0; prev = now; last = now;
const t = now / 1000;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
/* 水波:2026-10-04 先关后开 —— 用户之前看到的「宝宝脸/腿变形」其实是 GIF 自带的扭动(已停用 GIF),
这层只是叠加的柔光,不做任何形变;而且宝宝那一圈(guard)完全不叠,宝宝表情一点不受影响。 */
if ((tick ^= 1)) water(t);
drawWater();
rays(t);
dots(t, dt);
/* 宝宝保护:按当前阶段的宝宝轮廓把这一帧的水波/光束/微粒整块挖掉 ——
水在宝宝四周流,宝宝本身一像素都不叠(用户:「水波纹不能影响宝宝的表情,脸不能变」)。
孕囊在轻轻上下浮(CSS hbFloat),所以每帧现读位置。 */
if (babyMask) {
const hr = hero.getBoundingClientRect(), b = box.getBoundingClientRect();
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(babyMask, b.left - hr.left, b.top - hr.top, b.width, b.height);
}
}
function kick() { if (!raf) raf = requestAnimationFrame(frame); }
size();
addEventListener('resize', () => { size(); kick(); });
box.addEventListener('transitionend', size); // 换阶段后孕囊尺寸变了,保护区跟着挪
document.addEventListener('visibilitychange', kick);
if (home) new MutationObserver(kick).observe(home, { attributes: true, attributeFilter: ['class'] });
if ('IntersectionObserver' in window) new IntersectionObserver(es => { seen = es[0].isIntersecting; kick(); }).observe(hero);
cv.classList.add('is-on');
kick();
})();