Files
mama-box/js/hero-fx.js
Karlcarlo 6c656ecc28 style(首页): 头图柔光与柔和水波、宝宝不再形变;胎动踢出明显褶皱、加快连踢 (v26100410)
- 光束画在 1/6 小画布再放大,宽而柔;水波改为低分辨率正弦大片光影,去掉白色网状焦散
- 去掉孕囊 SVG 位移滤镜;孕囊中心区域水波/光束/微粒避开,宝宝脸不受影响
- 胎动:鼓包外放射状褶皱(左上亮右下暗、三层虚边),每一拍重新踢出、更深
- 连踢加「噔噔噔」快节奏(.22~.26s),26~33 周出现概率 40%→50%

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

189 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-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);
/* ---------------- 阳光 + 水波 ----------------
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);
if ((tick ^= 1)) water(t); // 水波 15fps 算一次就够,每帧照画
drawWater();
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();
})();