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

275 lines
18 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;
}
/* ---- 星座牌(2026-10-04,用户:「按预产期推算宝宝是哪个星座,在羊水囊外边放上『巨蟹宝宝』之类,协调、字不要太大、高级一些」) ----
挂在 .hero-baby 里(跟着孕囊一起浮),落在孕囊右下角那块空角上。
只在孕期显示(未建档 / 产后不显示:产后应按真实生日,而这里只有预产期)。
预产期离星座分界 ≤2 天时写成「巨蟹·狮子宝宝」—— 预产期本来就是估算,别说得太死。 */
const ZOD = [ // [起始月, 起始日, 名, 符号] 按起始日期排
[1, 20, '水瓶', '♒'], [2, 19, '双鱼', '♓'], [3, 21, '白羊', '♈'], [4, 20, '金牛', '♉'], [5, 21, '双子', '♊'], [6, 22, '巨蟹', '♋'],
[7, 23, '狮子', '♌'], [8, 23, '处女', '♍'], [9, 23, '天秤', '♎'], [10, 24, '天蝎', '♏'], [11, 23, '射手', '♐'], [12, 22, '摩羯', '♑']];
function zodiacOf(y, m, d) {
const t = Date.UTC(y, m - 1, d);
let i = ZOD.length - 1; // 1/1~1/19 是摩羯
for (let k = 0; k < ZOD.length; k++) if (t >= Date.UTC(y, ZOD[k][0] - 1, ZOD[k][1])) i = k;
// 摩羯跨年:1 月里的摩羯从去年 12/22 起、到今年 1/20 止;12 月里的摩羯到明年 1/20 止
const jan = i === 11 && m === 1, day = 864e5;
const start = Date.UTC(jan ? y - 1 : y, ZOD[i][0] - 1, ZOD[i][1]);
const nx = ZOD[(i + 1) % 12], nextStart = Date.UTC(i === 11 && !jan ? y + 1 : y, nx[0] - 1, nx[1]);
const near = (t - start) / day < 2 ? (i + 11) % 12 : (nextStart - t) / day <= 2 ? (i + 1) % 12 : -1;
return { main: ZOD[i], alt: near >= 0 ? ZOD[near] : null, prevAlt: near === (i + 11) % 12 };
}
const zEl = document.createElement('span');
zEl.className = 'hero-zod'; zEl.setAttribute('aria-hidden', 'true');
box.appendChild(zEl);
function renderZod(d) {
const due = d && !d.empty && !d.postpartum && d.due;
if (!due) { zEl.classList.remove('is-on'); return; }
const z = zodiacOf(due[0], due[1], due[2]);
const pair = z.alt ? (z.prevAlt ? [z.alt, z.main] : [z.main, z.alt]) : [z.main];
// 图标用自绘四角星:星座符号(♋ 之类)在部分安卓上没有字形,会显示成方框
zEl.innerHTML = '<i><svg viewBox="0 0 12 12" width="8" height="8"><path d="M6 .6c.4 2.9 1.5 4 4.4 4.4v.1C7.5 5.5 6.4 6.6 6 9.4h-.1C5.5 6.6 4.4 5.5 1.6 5.1V5C4.4 4.6 5.5 3.5 5.9.6z" fill="currentColor"/></svg></i>' +
'<b>' + pair.map(x => x[2]).join('·') + '宝宝</b>';
zEl.classList.add('is-on');
}
function onPreg(d) { show(stageOf(force != null ? +force : (d && d.week))); renderZod(d); }
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');
let tick = 0, W = 0, H = 0, dpr = 1, seen = true, raf = 0, last = 0, prev = 0;
let SX = 0, SY = 0, SR = 1; // 孕囊圆心/半径(CSS px)
/* ---- 羊水深度层(2026-10-04 五版,用户:「要羊水那种有深度的感觉,不是白色纹路来回跑」) ----
#heroWater 在宝宝**下面**、正常混合(#heroFx 是 screen 混合,只能提亮 —— 上一版的暗部根本显不出来,只剩白纹)。
① 折射:背景图按水平细条逐条错位(两组慢正弦),像隔着一层液体看过去,整片在缓缓荡
② 暗流:极低分辨率的大团深红褐色,慢慢涌动(只压暗、不发白),给出「水有厚度」的感觉
③ 景深:四周压深、上方略透亮(光从上面来),中间留给孕囊
④ 远处微粒:小而清楚、慢,在宝宝后面;近处的大而虚的光斑在 #heroFx 上、宝宝前面 → 前后有层次 */
const wcv = document.getElementById('heroWater'), wx = wcv && wcv.getContext('2d');
const bgImg = new Image(); bgImg.src = IMG + '6a268e7269b7.jpg';
const DW = 20, dcv = document.createElement('canvas'), dctx = dcv.getContext('2d');
let dh = 0, dimg = null;
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));
SX = b.left - r.left + b.width / 2; SY = b.top - r.top + b.height / 2; SR = Math.max(b.width, 60) / 2;
if (wcv) {
wcv.width = cv.width; wcv.height = cv.height;
dh = Math.max(8, Math.round(DW * H / W)); dcv.width = DW; dcv.height = dh; dimg = dctx.createImageData(DW, dh);
}
}
// 宝宝保护:孕囊中心附近 → 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 deep(t, dt) {
if (!wx) return;
const c = wx;
c.setTransform(dpr, 0, 0, dpr, 0, 0);
c.globalCompositeOperation = 'source-over'; c.globalAlpha = 1;
// 底色(同 CSS)
const base = c.createLinearGradient(0, 0, W, 0);
base.addColorStop(0, '#CE8069'); base.addColorStop(.3, '#D8886F'); base.addColorStop(1, '#DB9073');
c.fillStyle = base; c.fillRect(0, 0, W, H);
// ① 折射:背景图逐条错位(图高 = 头图高 - 12,即 CSS 里钉死的 250 + 安全区)
if (bgImg.complete && bgImg.naturalWidth) {
const A = 3.2, Hs = hero.classList.contains('no-letter') ? H - 12 : H;
const sc = Hs / bgImg.naturalHeight, dw = bgImg.naturalWidth * sc + A * 2, x0 = W - dw + A, step = 2;
for (let y = 0; y < Hs; y += step) {
const dx = A * Math.sin(y * .034 + t * .7) + A * .6 * Math.sin(y * .012 - t * .43);
c.drawImage(bgImg, 0, y / sc, bgImg.naturalWidth, step / sc, x0 + dx, y, dw, step + .5);
}
}
const ov = c.createLinearGradient(0, 0, W, 0);
ov.addColorStop(0, 'rgba(150,55,35,.20)'); ov.addColorStop(.38, 'rgba(150,55,35,.07)'); ov.addColorStop(.58, 'rgba(150,55,35,0)');
c.fillStyle = ov; c.fillRect(0, 0, W, H);
// ② 暗流:大团、慢、只压暗;偶尔一点琥珀色(不是白)
if ((tick ^= 1)) {
const d = dimg.data, T = t * .28;
for (let y = 0; y < dh; y++) for (let x = 0; x < DW; x++) {
const u = x / DW * 4, v = y / DW * 4;
const w = (Math.sin(u * 1.2 + T) + Math.sin(v * 1.5 - T * .7 + u * .6) + Math.sin((u + v) * .8 + T * .5)) / 3;
const o = (y * DW + x) * 4;
if (w < 0) { d[o] = 120; d[o + 1] = 36; d[o + 2] = 24; d[o + 3] = Math.min(1, -w * 1.5) * 70; }
else { d[o] = 255; d[o + 1] = 196; d[o + 2] = 140; d[o + 3] = w * w * 34; }
}
dctx.putImageData(dimg, 0, 0);
}
c.imageSmoothingEnabled = true; c.imageSmoothingQuality = 'high';
c.drawImage(dcv, 0, 0, W, H);
// ③ 景深:四周压深、上方略透亮
const vg = c.createRadialGradient(SX, SY, SR * .6, SX, SY, Math.max(W, H) * .95);
vg.addColorStop(0, 'rgba(110,30,18,0)'); vg.addColorStop(1, 'rgba(110,30,18,.32)');
c.fillStyle = vg; c.fillRect(0, 0, W, H);
const tg = c.createLinearGradient(0, 0, 0, H);
tg.addColorStop(0, 'rgba(255,214,170,.10)'); tg.addColorStop(.45, 'rgba(255,214,170,0)'); tg.addColorStop(1, 'rgba(105,28,16,.16)');
c.fillStyle = tg; c.fillRect(0, 0, W, H);
// ④ 远处微粒:小、清楚、慢
for (const p of FAR) {
p.x += (p.vx + Math.sin(t * .3 + p.ph) * .0012) * 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 al = .22 + .22 * Math.sin(t * p.sp + p.ph);
c.fillStyle = 'rgba(255,232,205,' + al.toFixed(3) + ')';
c.beginPath(); c.arc(p.x * W, p.y * H, p.r, 0, 6.2832); c.fill();
}
}
const FAR = Array.from({ length: 34 }, () => ({ x: Math.random(), y: Math.random(), r: .45 + Math.random() * .7,
vx: (Math.random() - .5) * .003, vy: -(.002 + Math.random() * .004), ph: Math.random() * 6.3, sp: .5 + Math.random() }));
// 柔光束:光源在头图上方偏右画外;每道是一条很宽的渐变扇形,画在小画布上再放大
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,226,190,' + (.30 * k).toFixed(3) + ')');
gr.addColorStop(.65, 'rgba(255,218,180,' + (.13 * 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);
}
// 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;孕囊中心附近不画,免得落在宝宝脸上
// 近处光斑(宝宝前面):少、大、很虚、暖色,漂得比远处快 → 和 deep() 里的远处微粒拉开前后
const DOTS = Array.from({ length: 9 }, () => ({ x: Math.random(), y: Math.random(), r: 2.2 + Math.random() * 3.5,
vx: (Math.random() - .5) * .01, vy: -(.006 + Math.random() * .01), ph: Math.random() * 6.3, sp: .4 + Math.random() * .8 }));
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 = (.06 + .08 * (.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,226,196,' + al.toFixed(3) + ')');
g.addColorStop(1, 'rgba(255,226,196,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);
deep(t, dt); // 羊水深度层(宝宝下面)
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');
if (wcv) { if (bgImg.complete && bgImg.naturalWidth) wcv.classList.add('is-on'); else bgImg.onload = () => wcv.classList.add('is-on'); } // 背景图到了再盖上,免得先出一块纯色
kick();
})();