220 lines
14 KiB
JavaScript
220 lines
14 KiB
JavaScript
/* ================= 首页头图:按孕周换宝宝 + 阳光水波(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);
|
||
box.classList.toggle('is-big', STAGES[i][0] >= 24); // 24 周起孕囊大,星座牌改压在囊边(见 css .hero-baby.is-big .hero-zod)
|
||
// 先把旧图淡掉再换,避免新旧宝宝叠在一起闪一下
|
||
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]);
|
||
/* 2026-10-04 用户:「出现『摩羯·水瓶宝宝』相当于两个星座,必须只能是一个」→ 只按预产期落在哪个区间取一个,
|
||
不再在分界 ±2 天写双星座(zodiacOf 的 alt 留着不用) */
|
||
const pair = [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]);
|
||
// 实体卡片按「象」上色(火 / 土 / 风 / 水,同弹窗主题;见 css .hero-zod.el-*)
|
||
zEl.className = 'hero-zod el-' + ((window.ZODIAC_ELEMENT || {})[zNames[0]] || 'fire') + ' is-on';
|
||
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');
|
||
})();
|