Files
mama-box/js/hero-fx.js
Karlcarlo ca164bc2f8 style(首页头图): 36 周起的宝宝改成头朝上 (v26100408)
用户反馈头朝下看着不舒服;重画足月宝宝(头朝上侧身蜷缩、闭眼闭嘴、撑满羊水囊),重新生成动图与静图。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:20:45 +00:00

139 lines
7.3 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, 孕囊倍数 k]
const STAGES = [
[0, '6fff51c80fe6.jpg', '2ad895e978db.gif', .42], // 1 4–7 周 胚芽 + 卵黄囊
[8, 'a75f4970478c.jpg', '90bd965c9c20.gif', .52], // 2 8–10 周 小豆芽胚胎
[11, '42d00e242622.jpg', 'd7eda80e7d45.gif', .62], // 3 11–13 周 刚有人形
[14, '23302f9bfb42.jpg', 'f8252c0c0c22.gif', .72], // 4 14–17 周 身子拉长、吮手
[18, '7e6c3509097a.jpg', '993c4174c867.gif', .86], // 5 18–23 周 比例接近新生儿、偏瘦
[24, 'c0cbdd749ab0.jpg', '9c682de32bc3.gif', 1], // 6 24–27 周 原来那个宝宝
[28, 'bbaebc03aaf7.jpg', '01529c30ecc6.gif', 1.01], // 7 28–31 周 大一圈、有胎发
[32, '086e4e001a89.jpg', '11061c1c24c1.gif', 1.02], // 8 32–35 周 肉嘟嘟、抱腿
[36, '66b149335975.jpg', '291f74fb9d03.gif', 1.03] // 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;
function show(i) {
if (i === cur) return;
cur = i;
const [, png, gif, k] = STAGES[i], my = ++seq;
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;
if (reduce) 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);
/* ---------------- 阳光 + 水波 ---------------- */
const cv = document.getElementById('heroFx');
if (!cv || reduce) return;
const hero = cv.parentNode, home = document.getElementById('screen-home');
const ctx = cv.getContext('2d');
const CW = 72; // 焦散低分辨率宽
const off = document.createElement('canvas'), oc = off.getContext('2d');
let W = 0, H = 0, dpr = 1, ch = 0, img = null, seen = true, raf = 0, last = 0;
function size() {
const r = hero.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);
ch = Math.max(8, Math.round(CW * H / W));
off.width = CW; off.height = ch; img = oc.createImageData(CW, ch);
}
// 光束:从右上角画外斜着照进来,每道各自慢慢摆、忽明忽暗
const RAYS = [[-.10, .07, .55], [.02, .045, .4], [.12, .09, .5], [.24, .05, .35], [.34, .07, .3]];
function rays(t) {
const ox = W * .92, oy = -H * .55, L = H * 2.1;
ctx.globalCompositeOperation = 'lighter';
RAYS.forEach(([a0, w, al], i) => {
const a = Math.PI * .62 + a0 + Math.sin(t * .13 + i * 1.7) * .035;
const ww = w * (1 + .25 * Math.sin(t * .21 + i));
const k = al * (.65 + .35 * Math.sin(t * .37 + i * 2.3));
const x1 = ox + Math.cos(a - ww) * L, y1 = oy + Math.sin(a - ww) * L;
const x2 = ox + Math.cos(a + ww) * L, y2 = oy + Math.sin(a + ww) * L;
const gr = ctx.createLinearGradient(ox, oy, ox + Math.cos(a) * L * .8, oy + Math.sin(a) * L * .8);
gr.addColorStop(0, 'rgba(255,248,225,0)');
gr.addColorStop(.3, 'rgba(255,244,215,' + (.16 * k).toFixed(3) + ')');
gr.addColorStop(.62, 'rgba(255,236,205,' + (.07 * k).toFixed(3) + ')');
gr.addColorStop(1, 'rgba(255,230,200,0)');
ctx.fillStyle = gr;
ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath(); ctx.fill();
});
}
// 水波焦散:几层正弦叠出起伏,取「接近 0 的那条线」做亮线,越往下越淡
function caustics(t) {
const d = img.data, cx = CW * .7, cy = ch * .1;
for (let y = 0; y < ch; y++) {
const fall = 1 - y / ch * .55;
for (let x = 0; x < CW; x++) {
const v = Math.sin(x * .23 + t * .8) + Math.sin(y * .31 - t * .6 + x * .05)
+ Math.sin((x + y) * .14 + t * .45) + Math.sin(Math.hypot(x - cx, y - cy) * .27 - t * 1.05);
let c = 1 - Math.abs(Math.sin(v * 1.15));
c = c * c; c = c * c * c; // 六次方:只留细细的亮网
const o = (y * CW + x) * 4;
d[o] = 255; d[o + 1] = 242; d[o + 2] = 222; d[o + 3] = c * 120 * fall;
}
}
oc.putImageData(img, 0, 0);
ctx.globalCompositeOperation = 'lighter';
ctx.imageSmoothingEnabled = true;
ctx.globalAlpha = .55;
ctx.drawImage(off, 0, 0, W, H);
ctx.globalAlpha = 1;
}
function frame(now) {
raf = 0;
if (!(seen && !document.hidden && (!home || home.classList.contains('is-active')))) return;
raf = requestAnimationFrame(frame);
if (now - last < 33) return; // ≈30fps 足够,省电
last = now;
const t = now / 1000;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
caustics(t); rays(t);
}
function kick() { if (!raf) raf = requestAnimationFrame(frame); }
size();
addEventListener('resize', () => { size(); kick(); });
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();
})();