- 9 个阶段静图 + GIF 重做:抠出宝宝缩小后贴回空孕囊,孕囊外形/大小不动 - GIF 每帧底图用静图,羊水不晃,只有宝宝自己在动;帧差透明压缩,1~3MB → 250~490KB - hero-fx.js 不再画水波层(光束、微粒保留) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
11 KiB
JavaScript
188 lines
11 KiB
JavaScript
/* ================= 首页头图:按孕周换宝宝 + 阳光水波(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, 'd86caa477171.jpg', '482d0a3043b9.gif', .42], // 1 4–7 周 胚芽 + 卵黄囊
|
||
[8, '677339874dd7.jpg', '30163971ab67.gif', .52], // 2 8–10 周 小豆芽胚胎
|
||
[11, 'eed131630ed3.jpg', 'a59fd5aa0f28.gif', .62], // 3 11–13 周 刚有人形
|
||
[14, 'fdcdef9ad731.jpg', 'b6fdd2c43538.gif', .72], // 4 14–17 周 身子拉长、吮手
|
||
[18, '18022a7d97d3.jpg', 'ece5fb854939.gif', .86], // 5 18–23 周 比例接近新生儿、偏瘦
|
||
[24, '87dac10fb33c.jpg', '55c1e3d20b6d.gif', 1], // 6 24–27 周 原来那个宝宝
|
||
[28, 'd8eb9eb12eee.jpg', 'a0e633273ab4.gif', 1.01], // 7 28–31 周 大一圈、有胎发
|
||
[32, '99cf27ee651e.jpg', '9d1908a2aff3.gif', 1.02], // 8 32–35 周 肉嘟嘟、抱腿
|
||
[36, '6bd1f87ff5ab.jpg', 'c878649c8fee.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);
|
||
|
||
/* ---------------- 阳光 + 水波 ----------------
|
||
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 = 32; // 水波低分辨率宽(越低越柔)
|
||
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 < .45 ? 0 : d > .95 ? 1 : (d - .45) / .5;
|
||
}
|
||
// 水波:四层不同方向、不同速度的长波叠加,取正半部分 → 一片片柔和的亮斑缓缓流过
|
||
function water(t) {
|
||
const d = img.data, T = t * .45;
|
||
for (let y = 0; y < ch; y++) {
|
||
for (let x = 0; x < CW; x++) {
|
||
const u = x / CW * 6, v = y / CW * 6;
|
||
let w = Math.sin(u * 1.1 + T) + Math.sin(v * 1.3 - T * .8 + u * .4) + Math.sin((u - v) * .9 + T * .6) + Math.sin(Math.hypot(u - 4.5, v - 1) * 1.6 - T * 1.1);
|
||
w = w / 4; w = w > 0 ? w * w : 0; // 只留亮的那半,平方让过渡更软
|
||
const X = x / CW * W, Y = y / ch * H;
|
||
const g = .15 + .85 * guard(X, Y);
|
||
const o = (y * CW + x) * 4;
|
||
d[o] = 255; d[o + 1] = 238; d[o + 2] = 214; d[o + 3] = w * 200 * g * (1 - y / ch * .35);
|
||
}
|
||
}
|
||
oc.putImageData(img, 0, 0);
|
||
}
|
||
function drawWater() {
|
||
ctx.globalCompositeOperation = 'lighter';
|
||
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
|
||
ctx.globalAlpha = .55;
|
||
ctx.drawImage(off, 0, 0, W, H);
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
// 柔光束:光源在头图上方偏右画外;每道是一条很宽的渐变扇形,画在小画布上再放大
|
||
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) => {
|
||
const a = Math.PI * .6 + a0 + Math.sin(t * .09 + i * 1.9) * .03;
|
||
const k = al * (.6 + .4 * Math.sin(t * .23 + 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) continue;
|
||
const al = (.15 + .25 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * 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 用户:「水波纹那一层先去掉,动的是宝宝自己(腿脚、嘴角),不是水」→ 不再画水波
|
||
rays(t);
|
||
dots(t, dt);
|
||
}
|
||
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();
|
||
})();
|