/* ================= 首页头图:按孕周换宝宝 + 阳光水波(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(null = 只用静图), 孕囊倍数 k, 宝宝轮廓遮罩(水波/光挖掉宝宝用)] 2026-10-04:宝宝在囊里缩到原图的 70%,孕囊不变 const STAGES = [ [0, 'd953571ad48d.jpg', null, .42, '91f1472ee58a.png'], // 1 4–7 周 胚芽 + 卵黄囊 [8, 'dd756c516072.jpg', null, .52, '5b39ca21a42e.png'], // 2 8–10 周 小豆芽胚胎 [11, 'a6f52893ad1e.jpg', null, .62, '0dcddb83b038.png'], // 3 11–13 周 刚有人形 [14, '251ec778dc15.jpg', null, .72, 'd63b80896e8e.png'], // 4 14–17 周 身子拉长、吮手 [18, 'dcf2ae48e0ec.jpg', null, .86, '0022b97f8cc0.png'], // 5 18–23 周 比例接近新生儿、偏瘦 [24, 'b0e24b10aaee.jpg', null, 1, 'd75b2a87e805.png'], // 6 24–27 周 原来那个宝宝 [28, '85da5aaebb4f.jpg', null, 1.01, '507e24e95ef0.png'], // 7 28–31 周 大一圈、有胎发 [32, '0780b4698253.jpg', null, 1.02, '681b0bdb885e.png'], // 8 32–35 周 肉嘟嘟、抱腿 [36, 'ff112c222b1e.jpg', null, 1.03, '494179795c67.png'] // 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, babyMask = null; // babyMask:当前阶段宝宝轮廓(黑 + alpha),见 frame() 末尾 function show(i) { if (i === cur) return; cur = i; const [, png, gif, k, mk] = STAGES[i], my = ++seq; const mi = new Image(); mi.onload = () => { if (my === seq) babyMask = mi; }; mi.src = IMG + mk; 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; /* 2026-10-04 用户:「水波纹一出来,宝宝的脸型、腿型就变了」—— 那是 GIF 自带的整体扭动(生成动图的通病), 不是页面特效。先只用静图:宝宝一像素都不变形。gif 位传 null;以后有了只动手脚/嘴角的新动图再填回来。 */ if (reduce || !gif) return; const g = new Image(); g.onload = () => { if (my !== seq) return; kid.src = g.src; kid.classList.add('is-on'); }; g.src = IMG + gif; } /* ---- 星座牌(2026-10-04,用户:「按预产期推算宝宝是哪个星座,在羊水囊外边放上『巨蟹宝宝』之类,协调、字不要太大、高级一些」) ---- 挂在 .hero-baby 里(跟着孕囊一起浮),落在孕囊右下角那块空角上。 只在孕期显示(未建档 / 产后不显示:产后应按真实生日,而这里只有预产期)。 预产期离星座分界 ≤2 天时写成「巨蟹·狮子宝宝」—— 预产期本来就是估算,别说得太死。 */ const ZOD = [ // [起始月, 起始日, 名, 符号] 按起始日期排 [1, 20, '水瓶', '♒'], [2, 19, '双鱼', '♓'], [3, 21, '白羊', '♈'], [4, 20, '金牛', '♉'], [5, 21, '双子', '♊'], [6, 22, '巨蟹', '♋'], [7, 23, '狮子', '♌'], [8, 23, '处女', '♍'], [9, 23, '天秤', '♎'], [10, 24, '天蝎', '♏'], [11, 23, '射手', '♐'], [12, 22, '摩羯', '♑']]; function zodiacOf(y, m, d) { const t = Date.UTC(y, m - 1, d); let i = ZOD.length - 1; // 1/1~1/19 是摩羯 for (let k = 0; k < ZOD.length; k++) if (t >= Date.UTC(y, ZOD[k][0] - 1, ZOD[k][1])) i = k; // 摩羯跨年:1 月里的摩羯从去年 12/22 起、到今年 1/20 止;12 月里的摩羯到明年 1/20 止 const jan = i === 11 && m === 1, day = 864e5; const start = Date.UTC(jan ? y - 1 : y, ZOD[i][0] - 1, ZOD[i][1]); const nx = ZOD[(i + 1) % 12], nextStart = Date.UTC(i === 11 && !jan ? y + 1 : y, nx[0] - 1, nx[1]); const near = (t - start) / day < 2 ? (i + 11) % 12 : (nextStart - t) / day <= 2 ? (i + 1) % 12 : -1; return { main: ZOD[i], alt: near >= 0 ? ZOD[near] : null, prevAlt: near === (i + 11) % 12 }; } const zEl = document.createElement('span'); zEl.className = 'hero-zod'; zEl.setAttribute('aria-hidden', 'true'); box.appendChild(zEl); function renderZod(d) { const due = d && !d.empty && !d.postpartum && d.due; if (!due) { zEl.classList.remove('is-on'); return; } const z = zodiacOf(due[0], due[1], due[2]); const pair = z.alt ? (z.prevAlt ? [z.alt, z.main] : [z.main, z.alt]) : [z.main]; // 图标用自绘四角星:星座符号(♋ 之类)在部分安卓上没有字形,会显示成方框 zEl.innerHTML = '' + '' + pair.map(x => x[2]).join('·') + '宝宝'; zEl.classList.add('is-on'); } function onPreg(d) { show(stageOf(force != null ? +force : (d && d.week))); renderZod(d); } 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'); let tick = 0, W = 0, H = 0, dpr = 1, seen = true, raf = 0, last = 0, prev = 0; let SX = 0, SY = 0, SR = 1; // 孕囊圆心/半径(CSS px) /* ---- 羊水深度层(2026-10-04 五版,用户:「要羊水那种有深度的感觉,不是白色纹路来回跑」) ---- #heroWater 在宝宝**下面**、正常混合(#heroFx 是 screen 混合,只能提亮 —— 上一版的暗部根本显不出来,只剩白纹)。 ① 折射:背景图按水平细条逐条错位(两组慢正弦),像隔着一层液体看过去,整片在缓缓荡 ② 暗流:极低分辨率的大团深红褐色,慢慢涌动(只压暗、不发白),给出「水有厚度」的感觉 ③ 景深:四周压深、上方略透亮(光从上面来),中间留给孕囊 ④ 远处微粒:小而清楚、慢,在宝宝后面;近处的大而虚的光斑在 #heroFx 上、宝宝前面 → 前后有层次 */ const wcv = document.getElementById('heroWater'), wx = wcv && wcv.getContext('2d'); const bgImg = new Image(); bgImg.src = IMG + '6a268e7269b7.jpg'; const DW = 20, dcv = document.createElement('canvas'), dctx = dcv.getContext('2d'); let dh = 0, dimg = null; 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)); SX = b.left - r.left + b.width / 2; SY = b.top - r.top + b.height / 2; SR = Math.max(b.width, 60) / 2; if (wcv) { wcv.width = cv.width; wcv.height = cv.height; dh = Math.max(8, Math.round(DW * H / W)); dcv.width = DW; dcv.height = dh; dimg = dctx.createImageData(DW, dh); } } // 宝宝保护:孕囊中心附近 → 0,囊边 → 1 function guard(x, y) { const d = Math.hypot(x - SX, y - SY) / SR; return d < .62 ? 0 : d > 1.05 ? 1 : (d - .62) / .43; // 宝宝缩到 70% 后大约占孕囊半径的 6 成 } // 水波:四层不同方向、不同速度的长波叠加,取正半部分 → 一片片柔和的亮斑缓缓流过 function deep(t, dt) { if (!wx) return; const c = wx; c.setTransform(dpr, 0, 0, dpr, 0, 0); c.globalCompositeOperation = 'source-over'; c.globalAlpha = 1; // 底色(同 CSS) const base = c.createLinearGradient(0, 0, W, 0); base.addColorStop(0, '#CE8069'); base.addColorStop(.3, '#D8886F'); base.addColorStop(1, '#DB9073'); c.fillStyle = base; c.fillRect(0, 0, W, H); // ① 折射:背景图逐条错位(图高 = 头图高 - 12,即 CSS 里钉死的 250 + 安全区) if (bgImg.complete && bgImg.naturalWidth) { const A = 3.2, Hs = hero.classList.contains('no-letter') ? H - 12 : H; const sc = Hs / bgImg.naturalHeight, dw = bgImg.naturalWidth * sc + A * 2, x0 = W - dw + A, step = 2; for (let y = 0; y < Hs; y += step) { const dx = A * Math.sin(y * .034 + t * .7) + A * .6 * Math.sin(y * .012 - t * .43); c.drawImage(bgImg, 0, y / sc, bgImg.naturalWidth, step / sc, x0 + dx, y, dw, step + .5); } } const ov = c.createLinearGradient(0, 0, W, 0); ov.addColorStop(0, 'rgba(150,55,35,.20)'); ov.addColorStop(.38, 'rgba(150,55,35,.07)'); ov.addColorStop(.58, 'rgba(150,55,35,0)'); c.fillStyle = ov; c.fillRect(0, 0, W, H); // ② 暗流:大团、慢、只压暗;偶尔一点琥珀色(不是白) if ((tick ^= 1)) { const d = dimg.data, T = t * .28; for (let y = 0; y < dh; y++) for (let x = 0; x < DW; x++) { const u = x / DW * 4, v = y / DW * 4; const w = (Math.sin(u * 1.2 + T) + Math.sin(v * 1.5 - T * .7 + u * .6) + Math.sin((u + v) * .8 + T * .5)) / 3; const o = (y * DW + x) * 4; if (w < 0) { d[o] = 120; d[o + 1] = 36; d[o + 2] = 24; d[o + 3] = Math.min(1, -w * 1.5) * 70; } else { d[o] = 255; d[o + 1] = 196; d[o + 2] = 140; d[o + 3] = w * w * 34; } } dctx.putImageData(dimg, 0, 0); } c.imageSmoothingEnabled = true; c.imageSmoothingQuality = 'high'; c.drawImage(dcv, 0, 0, W, H); // ③ 景深:四周压深、上方略透亮 const vg = c.createRadialGradient(SX, SY, SR * .6, SX, SY, Math.max(W, H) * .95); vg.addColorStop(0, 'rgba(110,30,18,0)'); vg.addColorStop(1, 'rgba(110,30,18,.32)'); c.fillStyle = vg; c.fillRect(0, 0, W, H); const tg = c.createLinearGradient(0, 0, 0, H); tg.addColorStop(0, 'rgba(255,214,170,.10)'); tg.addColorStop(.45, 'rgba(255,214,170,0)'); tg.addColorStop(1, 'rgba(105,28,16,.16)'); c.fillStyle = tg; c.fillRect(0, 0, W, H); // ④ 远处微粒:小、清楚、慢 for (const p of FAR) { p.x += (p.vx + Math.sin(t * .3 + p.ph) * .0012) * 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 al = .22 + .22 * Math.sin(t * p.sp + p.ph); c.fillStyle = 'rgba(255,232,205,' + al.toFixed(3) + ')'; c.beginPath(); c.arc(p.x * W, p.y * H, p.r, 0, 6.2832); c.fill(); } } const FAR = Array.from({ length: 34 }, () => ({ x: Math.random(), y: Math.random(), r: .45 + Math.random() * .7, vx: (Math.random() - .5) * .003, vy: -(.002 + Math.random() * .004), ph: Math.random() * 6.3, sp: .5 + Math.random() })); // 柔光束:光源在头图上方偏右画外;每道是一条很宽的渐变扇形,画在小画布上再放大 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) => { // 2026-10-04 用户:「射下来的光束可以动一动」→ 摆幅 .03→.07、摆速加快,明暗起伏更明显 const a = Math.PI * .6 + a0 + Math.sin(t * .16 + i * 1.9) * .07; const k = al * (.45 + .55 * Math.sin(t * .35 + 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,226,190,' + (.30 * k).toFixed(3) + ')'); gr.addColorStop(.65, 'rgba(255,218,180,' + (.13 * 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); } // 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;孕囊中心附近不画,免得落在宝宝脸上 // 近处光斑(宝宝前面):少、大、很虚、暖色,漂得比远处快 → 和 deep() 里的远处微粒拉开前后 const DOTS = Array.from({ length: 9 }, () => ({ x: Math.random(), y: Math.random(), r: 2.2 + Math.random() * 3.5, vx: (Math.random() - .5) * .01, vy: -(.006 + Math.random() * .01), ph: Math.random() * 6.3, sp: .4 + Math.random() * .8 })); 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 && !babyMask) continue; const al = (.06 + .08 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * (babyMask ? 1 : gd); const R = p.r * 2.4, g = ctx.createRadialGradient(X, Y, 0, X, Y, R); g.addColorStop(0, 'rgba(255,226,196,' + al.toFixed(3) + ')'); g.addColorStop(1, 'rgba(255,226,196,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); deep(t, dt); // 羊水深度层(宝宝下面) rays(t); dots(t, dt); /* 宝宝保护:按当前阶段的宝宝轮廓把这一帧的水波/光束/微粒整块挖掉 —— 水在宝宝四周流,宝宝本身一像素都不叠(用户:「水波纹不能影响宝宝的表情,脸不能变」)。 孕囊在轻轻上下浮(CSS hbFloat),所以每帧现读位置。 */ if (babyMask) { const hr = hero.getBoundingClientRect(), b = box.getBoundingClientRect(); ctx.globalCompositeOperation = 'destination-out'; ctx.drawImage(babyMask, b.left - hr.left, b.top - hr.top, b.width, b.height); } } 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'); if (wcv) { if (bgImg.complete && bgImg.naturalWidth) wcv.classList.add('is-on'); else bgImg.onload = () => wcv.classList.add('is-on'); } // 背景图到了再盖上,免得先出一块纯色 kick(); })();