Files
mama-box/js/hero-fx.js

215 lines
14 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 周显示。
② 羊水 + 光束:#heroWater 一张 WebGL 画布(搬自参考页 womb-water.js),在孕囊下面。
柔光束 + 柔和水波光影 + 悬浮微粒,宝宝区域避开(详见下面那段)。
不在首页 / 页面隐藏 / 头图滚出视口时停画;系统「减少动态效果」下整块不画(CSS 也隐藏)。 */
(function () {
'use strict';
// [起始孕周, 静图, GIF(null = 只用静图), 孕囊倍数 k] 静图是透明底 PNG(囊壁外渐隐),叠在羊水层上 2026-10-04:宝宝在囊里缩到原图的 70%,孕囊不变
const STAGES = [
[0, 'c685f94d437c.png', null, .42], // 1 4–7 周 胚芽 + 卵黄囊
[8, '6c5d82c9b093.png', null, .52], // 2 8–10 周 小豆芽胚胎
[11, '450581c2f05f.png', null, .62], // 3 11–13 周 刚有人形
[14, 'dc4c7eea1c50.png', null, .72], // 4 14–17 周 身子拉长、吮手
[18, '7bf792b04123.png', null, .86], // 5 18–23 周 比例接近新生儿、偏瘦
[24, 'f140f4367b48.png', null, 1], // 6 24–27 周 原来那个宝宝
[28, '51e99b3060e2.png', null, 1.01], // 7 28–31 周 大一圈、有胎发
[32, 'a5dbb24c1ef2.png', null, 1.02], // 8 32–35 周 肉嘟嘟、抱腿
[36, '610c4d92c16d.png', null, 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;
/* 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);
zEl.setAttribute('role', 'button'); zEl.removeAttribute('aria-hidden');
let zNames = [];
zEl.addEventListener('click', e => { e.stopPropagation(); if (zNames.length) window.dispatchEvent(new CustomEvent('mb-zodiac', { detail: { names: zNames } })); });
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];
// 图标:对应星座的自绘符号(js/zodiac-art.js;不用 ♋ 一类字符,部分安卓没有字形会成方框)
zEl.innerHTML = '<i>' + (window.zodiacIcon ? window.zodiacIcon(pair[0][2], 10, 2.6) : '') + '</i>' +
'<b>' + pair.map(x => x[2]).join('·') + '宝宝</b>';
zNames = pair.map(x => x[2]);
zEl.setAttribute('aria-label', zNames.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 七版) ----------------
用户:「按我给的网址改头部,主要是它的光束和羊水,右上角的羊水比较好,用那一部分放到现在的头部」
参考页:beian3.douba.top/YANG-bak-10041632/?v=2(js/womb-water.js,WebGL)。
这里**原样搬了它的 fragment shader**:羊水底色 + 整体慢晃 + 折射波纹、右半边斜射的光束和焦散光纹、
背后会「呼吸」的暖光、上浮颗粒、暗角。只改两处:
· 暖光中心对齐到我们的孕囊(uC,每帧读 .hero-baby 的位置,跟着孕囊浮动走)
· 孕周从 mb-preg 事件拿(参考页读 #heroWeek 文字)
画布 #heroWater 在 .hero-baby 下面(z1),孕囊 + 宝宝(透明底 PNG)叠在上面 → 宝宝一像素都不受影响;
LOGO / 文案(z3)在最上面。之前自己写的 2D 水层、光束、光斑整段删掉(不叠两套)。
没有 WebGL → 什么都不画,CSS 背景照常;系统「减少动态效果」→ 只画一帧。 */
const cv = document.getElementById('heroWater');
if (!cv) return;
const hero = cv.parentNode, home = document.getElementById('screen-home');
const gl = cv.getContext('webgl', { antialias: false, alpha: false }) || cv.getContext('experimental-webgl');
if (!gl) return;
const VS = 'attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}';
const FS = [
'precision mediump float;',
'uniform vec2 uRes,uC;uniform float uT,uDpr,uW;',
'float h1(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
'float vn(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);',
' return mix(mix(h1(i),h1(i+vec2(1,0)),f.x),mix(h1(i+vec2(0,1)),h1(i+vec2(1,1)),f.x),f.y);}',
'float fbm(vec2 p){float v=0.,a=.5;for(int i=0;i<4;i++){v+=a*vn(p);p=p*2.03+vec2(1.7,9.2);a*=.5;}return v;}',
'void main(){',
' vec2 p=vec2(gl_FragCoord.x,uRes.y*uDpr-gl_FragCoord.y)/uDpr;',
' vec2 q=p/uRes;float A=uRes.x/uRes.y;float t=uT;',
// 水在晃:整体慢漂 + 折射波纹
' vec2 sw=vec2(sin(t*.55)*.018+sin(t*.23)*.008,cos(t*.43)*.014);',
' vec2 fl=vec2(fbm(q*vec2(A,1.)*1.6+vec2(t*.12,0.)),fbm(q*vec2(A,1.)*1.6+vec2(5.2,-t*.10)))-.5;',
' vec2 qd=q+sw+.028*fl+.009*vec2(sin(q.y*14.+t*1.8),cos(q.x*A*10.+t*1.5));',
// 羊水底色:头图同色系,上浅下深;左边(文案区)再压一点暖色
' vec3 c=mix(vec3(.93,.64,.52),vec3(.79,.43,.36),smoothstep(-.15,1.05,qd.y));',
' c=mix(c,vec3(.84,.49,.40),.35*(1.-smoothstep(.0,.6,q.x)));',
' c+=.07*(fbm(qd*vec2(A,1.)*3.+vec2(t*.09,-t*.07))-.5);',
// 光只在右半边:左上角是 LOGO + slogan,那里一点光都不给
' float lk=smoothstep(.38,.75,q.x);',
' vec2 cp=vec2(qd.x*A*3.,qd.y*7.);',
' float a1=fbm(cp+vec2(t*.30,t*.18)),a2=fbm(cp*1.3-vec2(t*.26,-t*.22)+4.);',
' float ca=1.-smoothstep(0.,.06,abs(a1-a2));',
' c+=vec3(1.,.90,.74)*ca*pow(clamp(1.-q.y/.5,0.,1.),2.)*.22*lk;',
// 阳光:隐隐约约的几道,慢慢起伏
' vec2 r=qd-vec2(.74,-.45);float an=atan(r.x*A,r.y);',
' float ry=vn(vec2(an*7.,t*.28))*vn(vec2(an*17.,-t*.33)+3.);',
' ry=smoothstep(.15,.75,ry)*smoothstep(1.6,.2,length(r))*lk;',
' float sun=.75+.25*sin(t*.6);',
' c+=vec3(1.,.87,.66)*ry*.30*sun;',
// ── 宝宝剪影已去掉(2026-10-04 用户要求),只留原来剪影背后那团会「呼吸」的暖光,位置和浮动照旧 ──
' float m=smoothstep(14.,40.,uW);',
' vec2 bc=uC+vec2(0.,uRes.y*.03*m);',
' float bob=uRes.y*.022*sin(t*.55+.8);',
' float gd=length((p-bc-vec2(0.,bob))/uRes.y);',
' float br=.8+.2*sin(t*1.1);',
' c+=vec3(1.,.83,.62)*(.22*exp(-gd*3.2)+.10*exp(-gd*1.2))*br;',
// 上浮颗粒(只要很少几颗)
' for(int i=0;i<2;i++){float Lr=float(i);',
' vec2 g=q*vec2(A,1.)*(9.+Lr*7.)+vec2(sin(t*.4+Lr)*.3,t*(.30+Lr*.22));',
' vec2 id=floor(g),f=fract(g)-.5;float rn=h1(id+Lr*7.);',
' vec2 o=vec2(h1(id+3.1),h1(id+5.7))-.5;float dd=length(f-o*.6);',
' float tw=.6+.4*sin(t*2.+rn*30.);',
' c+=vec3(1.,.94,.84)*smoothstep(.05+.03*rn,0.,dd)*step(.80,rn)*(.35-Lr*.15)*tw;}',
' c*=1.-.12*pow(length((q-vec2(.55,.4))*vec2(1.,1.3)),2.);',
' gl_FragColor=vec4(c,1.);}'
].join('\n');
function sh(type, src) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
return s;
}
let pr;
try {
pr = gl.createProgram();
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS));
gl.linkProgram(pr);
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(pr));
} catch (err) { console.warn('[hero-water]', err); return; }
gl.useProgram(pr);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(pr, 'a');
gl.enableVertexAttribArray(aLoc); gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
const U = {}; ['uRes', 'uC', 'uT', 'uDpr', 'uW'].forEach(n => { U[n] = gl.getUniformLocation(pr, n); });
const dpr = Math.min(devicePixelRatio || 1, 2), t0 = performance.now();
let W = 0, H = 0, raf = 0, seen = true, week = 20;
addEventListener('mb-preg', e => { const w = e.detail && e.detail.week; week = w > 0 ? w : 20; });
if (window.__mbPreg && window.__mbPreg.week > 0) week = window.__mbPreg.week;
function resize() {
const r = hero.getBoundingClientRect(); if (!r.width) return;
W = r.width; H = r.height;
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
gl.viewport(0, 0, cv.width, cv.height);
}
function draw(now) {
const hr = hero.getBoundingClientRect(), b = box.getBoundingClientRect();
gl.uniform2f(U.uRes, W, H); gl.uniform1f(U.uDpr, dpr);
gl.uniform2f(U.uC, b.left - hr.left + b.width / 2, b.top - hr.top + b.height / 2);
gl.uniform1f(U.uW, Math.min(42, Math.max(5, week)));
gl.uniform1f(U.uT, reduce ? 2 : (now - t0) / 1000);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
function loop(now) {
raf = 0;
if (!(seen && !document.hidden && (!home || home.classList.contains('is-active')))) return;
draw(now);
if (!reduce) raf = requestAnimationFrame(loop);
}
function kick() { if (!raf) raf = requestAnimationFrame(loop); }
resize(); kick();
addEventListener('resize', () => { resize(); 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; if (seen) { resize(); kick(); } }).observe(hero);
if (reduce) addEventListener('mb-preg', kick);
cv.addEventListener('webglcontextlost', () => { cancelAnimationFrame(raf); cv.classList.remove('is-on'); });
cv.classList.add('is-on');
})();