- 背景换成去掉羊水囊的空背景,羊水囊+宝宝整幅按阶段换(4/8/11/14/18/24/28/32/36 周) - 每阶段一张本地程序化形变 GIF(呼吸、轻浮、小手、蹬脚;脸不动,闭眼闭嘴)+ 同帧静图打底 - 孕囊直径按阶段 .42~1.03 倍,围绕原圆心缩放 - 去掉花瓣飘落;新增 #heroFx 画布:斜照光束 + 水波焦散,离开首页/不可见时停画 - 逻辑独立在 js/hero-fx.js,调试 ?kidweek=N Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
7.2 KiB
JavaScript
139 lines
7.2 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, '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, 'f6d3a8d56d5b.jpg', '2262d424d7e2.gif', 1.03] // 9 36 周起 头朝下入盆
|
||
];
|
||
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();
|
||
})();
|