/* ================= 首页头图:按孕周换宝宝 + 阳光水波(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); /* ---------------- 阳光 + 水波 ---------------- */ 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(); })();