Files
mama-box/js/hero-fx.js
Karlcarlo 3a3ea0a393 style(首页头图): 羊水质感升级——顶部光束、真实水底焦散、液体折射、悬浮微粒 (v26100409)
- 焦散改为游动点细胞边界 + 正弦扭曲的不规则细亮网,两层叠加,囊内更亮
- 光束从顶部斜射,三层软边,摆动明暗;亮度加强
- .hero-baby 挂 SVG 位移滤镜 #mbFluid,羊水折射式轻微晃动
- 新增 34 颗悬浮微粒,经过光束时更亮

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

223 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 混合叠在宝宝上、文字下。
光束从顶上打进来 + 水底焦散 + 悬浮微粒;羊水囊整体挂 SVG 位移滤镜做液体折射(详见下面那段)。
不在首页 / 页面隐藏 / 头图滚出视口时停画;系统「减少动态效果」下整块不画(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 二版(用户:「水波纹上面要有光束打进来,水波纹要模拟真实羊水的感觉」):
· 折射:.hero-baby(羊水囊 + 宝宝)挂一个 SVG 位移滤镜,噪声图慢慢变形 → 隔着一层液体看的那种轻微晃动
· 焦散:水底那种不规则、不停变形的细亮网(游动点的细胞边界 + 正弦扭曲),低分辨率算完放大,囊附近更亮
· 光束:从顶上斜着打进来,三层叠出软边,各自慢慢摆、忽明忽暗,越往下越散
· 悬浮微粒:羊水里的小颗粒,慢慢漂、闪一下,经过光束时更亮 */
const cv = document.getElementById('heroFx');
if (!cv || reduce) return;
const hero = cv.parentNode, home = document.getElementById('screen-home');
const ctx = cv.getContext('2d');
// 折射滤镜:baseFrequency 在 16 秒里来回变 → 噪声缓缓「流动」;位移 5px,看得出是液体,又不至于把宝宝扭坏
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
svg.setAttribute('aria-hidden', 'true'); svg.style.position = 'absolute';
svg.innerHTML = '<filter id="mbFluid" x="-4%" y="-4%" width="108%" height="108%" color-interpolation-filters="sRGB">' +
'<feTurbulence type="fractalNoise" baseFrequency="0.011 0.016" numOctaves="2" seed="7" result="n">' +
'<animate attributeName="baseFrequency" dur="16s" values="0.011 0.016;0.016 0.011;0.013 0.018;0.011 0.016" repeatCount="indefinite"/>' +
'</feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/></filter>';
document.body.appendChild(svg);
box.classList.add('is-fluid');
const CW = 96; // 焦散低分辨率宽(太低会糊成团,看不出是光网)
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);
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 的地方最亮),
// 坐标再加一点正弦扭曲让边线弯起来;大小两层叠加,一层快一层慢,网格才会像水一样不停变形
function mkLayer(n, cell, seed) {
let r = seed;
const rnd = () => ((r = (r * 16807) % 2147483647) / 2147483647);
return { cell, pts: Array.from({ length: n }, () => [rnd(), rnd(), rnd() * 6.28, .35 + rnd() * .5]) };
}
const LAYERS = [[mkLayer(26, 22, 11), .8, 1], [mkLayer(14, 32, 29), .5, .8]];
const PX = new Float32Array(40), PY = new Float32Array(40);
function caustics(t) {
const d = img.data, sx = SX / W * CW, sy = SY / H * ch, sr = SR / W * CW;
const acc = caustics.acc && caustics.acc.length === CW * ch ? caustics.acc : (caustics.acc = new Float32Array(CW * ch));
acc.fill(0);
const k = CW / 120;
for (const [L, gain, speed] of LAYERS) {
const tt = t * speed, cell = L.cell * k, sg = 1 / (2 * (cell * .06) ** 2), n = L.pts.length;
for (let i = 0; i < n; i++) {
const [bx, by, ph, sp] = L.pts[i];
PX[i] = bx * CW + Math.cos(tt * sp + ph) * cell * .35;
PY[i] = by * ch + Math.sin(tt * sp * 1.3 + ph) * cell * .35;
}
for (let y = 0; y < ch; y++) {
const ox = Math.sin(y * .11 / k + tt * .7) * 2.2 * k;
for (let x = 0; x < CW; x++) {
const wx = x + ox, wy = y + Math.sin(x * .09 / k - tt * .6) * 2.2 * k;
let m1 = 1e9, m2 = 1e9;
for (let i = 0; i < n; i++) {
const dx = wx - PX[i], dy = wy - PY[i], q = dx * dx + dy * dy;
if (q < m1) { m2 = m1; m1 = q; } else if (q < m2) m2 = q;
}
const e = Math.sqrt(m2) - Math.sqrt(m1);
acc[y * CW + x] += Math.exp(-e * e * sg) * gain;
}
}
}
for (let y = 0; y < ch; y++) {
for (let x = 0; x < CW; x++) {
let v = acc[y * CW + x]; v = v >= 1 ? 1 : Math.pow(v, 1.3);
// 囊里亮、囊外淡,上半部比下半部亮(光从上面来)
const dd = Math.hypot(x - sx, y - sy) / sr;
const w = (.3 + .7 * Math.max(0, 1 - Math.max(0, dd - .6) * 1.8)) * (1 - y / ch * .45);
const o = (y * CW + x) * 4;
d[o] = 255; d[o + 1] = 240; d[o + 2] = 220; d[o + 3] = v * 255 * w;
}
}
oc.putImageData(img, 0, 0);
}
function drawCaustics() {
ctx.globalCompositeOperation = 'lighter';
ctx.imageSmoothingEnabled = true;
ctx.globalAlpha = 1;
ctx.drawImage(off, 0, 0, W, H);
ctx.globalAlpha = 1;
}
// 光束:光源在头图上方偏右的画外,斜着往左下打;每道画三层(宽/中/窄)叠出软边
const RAYS = [[-.16, .030, .7], [-.07, .018, .9], [.01, .040, .6], [.09, .022, 1], [.17, .032, .7], [.26, .02, .55]];
let rayA = [];
function rays(t) {
const ox = W * .78, oy = -H * .9, L = H * 2.6;
ctx.globalCompositeOperation = 'lighter';
rayA = [];
RAYS.forEach(([a0, w, al], i) => {
const a = Math.PI * .58 + a0 + Math.sin(t * .11 + i * 1.9) * .025;
const k = al * (.55 + .45 * Math.sin(t * .29 + i * 2.1)) * (.85 + .15 * Math.sin(t * 1.3 + i * 3.7));
rayA.push([a, w, k]);
[[2.2, .25], [1.3, .5], [.6, 1]].forEach(([wm, am]) => {
const ww = w * wm;
const gr = ctx.createLinearGradient(ox, oy, ox + Math.cos(a) * L * .75, oy + Math.sin(a) * L * .75);
gr.addColorStop(0, 'rgba(255,250,230,0)');
gr.addColorStop(.36, 'rgba(255,248,226,' + (.42 * k * am).toFixed(3) + ')');
gr.addColorStop(.62, 'rgba(255,242,215,' + (.22 * k * am).toFixed(3) + ')');
gr.addColorStop(1, 'rgba(255,232,200,0)');
ctx.fillStyle = gr;
ctx.beginPath(); ctx.moveTo(ox, oy);
ctx.lineTo(ox + Math.cos(a - ww) * L, oy + Math.sin(a - ww) * L);
ctx.lineTo(ox + Math.cos(a + ww) * L, oy + Math.sin(a + ww) * L);
ctx.closePath(); ctx.fill();
});
});
return [ox, oy];
}
// 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;落在光束里更亮
const DOTS = Array.from({ length: 34 }, () => ({ x: Math.random(), y: Math.random(), r: .5 + Math.random() * 1.4,
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, o) {
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;
let lit = 0;
const ang = Math.atan2(Y - o[1], X - o[0]);
for (const [a, w, k] of rayA) { const d = Math.abs(ang - a); if (d < w * 1.3) lit = Math.max(lit, k * (1 - d / (w * 1.3))); }
const al = (.18 + .3 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * (1 + lit * 1.6);
const R = p.r * 2.4, g = ctx.createRadialGradient(X, Y, 0, X, Y, R);
g.addColorStop(0, 'rgba(255,250,238,' + Math.min(al, .9).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)) caustics(t); // 焦散 15fps 算一次就够(水底光网本来就慢),每帧照画
drawCaustics();
const o = rays(t);
dots(t, dt, o);
}
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();
})();