style(首页): 头图柔光与柔和水波、宝宝不再形变;胎动踢出明显褶皱、加快连踢 (v26100410)

- 光束画在 1/6 小画布再放大,宽而柔;水波改为低分辨率正弦大片光影,去掉白色网状焦散
- 去掉孕囊 SVG 位移滤镜;孕囊中心区域水波/光束/微粒避开,宝宝脸不受影响
- 胎动:鼓包外放射状褶皱(左上亮右下暗、三层虚边),每一拍重新踢出、更深
- 连踢加「噔噔噔」快节奏(.22~.26s),26~33 周出现概率 40%→50%

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 02:48:00 +00:00
parent 3a3ea0a393
commit 6c656ecc28
5 changed files with 120 additions and 110 deletions

View File

@@ -7,7 +7,7 @@
孕囊大小 = .hero-baby 的 --k(相对原 25 周囊的倍数),CSS 里围绕同一圆心缩放、带过渡。
调试:URL 带 kidweek=12 强制按 12 周显示。
② 阳光水波:#heroFx 一张画布,screen 混合叠在宝宝上、文字下。
光束从顶上打进来 + 水底焦散 + 悬浮微粒;羊水囊整体挂 SVG 位移滤镜做液体折射(详见下面那段)。
柔光束 + 柔和水波光影 + 悬浮微粒,宝宝区域避开(详见下面那段)。
不在首页 / 页面隐藏 / 头图滚出视口时停画;系统「减少动态效果」下整块不画(CSS 也隐藏)。 */
(function () {
'use strict';
@@ -60,138 +60,104 @@
else setTimeout(() => { if (cur < 0) show(DEFAULT); }, 2500);
/* ---------------- 阳光 + 水波 ----------------
2026-10-04 二版(用户:「水波纹上面要有光束打进来,水波纹要模拟真实羊水的感觉」):
· 折射:.hero-baby(羊水囊 + 宝宝)挂一个 SVG 位移滤镜,噪声图慢慢变形 → 隔着一层液体看的那种轻微晃动
· 焦散:水底那种不规则、不停变形的细亮网(游动点的细胞边界 + 正弦扭曲),低分辨率算完放大,囊附近更亮
· 光束:从顶上斜着打进来,三层叠出软边,各自慢慢摆、忽明忽暗,越往下越散
· 悬浮微粒:羊水里的小颗粒,慢慢漂、闪一下,经过光束时更亮 */
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');
// 折射滤镜:baseFrequency 在 16 秒里来回变 → 噪声缓缓「流动」;位移 5px,看得出是液体,又不至于把宝宝扭坏
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
svg.setAttribute('aria-hidden', 'true'); svg.style.position = 'absolute';
svg.innerHTML = '<filter id="mbFluid" x="-4%" y="-4%" width="108%" height="108%" color-interpolation-filters="sRGB">' +
'<feTurbulence type="fractalNoise" baseFrequency="0.011 0.016" numOctaves="2" seed="7" result="n">' +
'<animate attributeName="baseFrequency" dur="16s" values="0.011 0.016;0.016 0.011;0.013 0.018;0.011 0.016" repeatCount="indefinite"/>' +
'</feTurbulence><feDisplacementMap in="SourceGraphic" in2="n" scale="5" xChannelSelector="R" yChannelSelector="G"/></filter>';
document.body.appendChild(svg);
box.classList.add('is-fluid');
const CW = 96; // 焦散低分辨率宽(太低会糊成团,看不出是光网)
const RS = 6; // 光束小画布缩小倍数
const rc = document.createElement('canvas'), rx = rc.getContext('2d');
const CW = 32; // 水波低分辨率宽(越低越柔)
const off = document.createElement('canvas'), oc = off.getContext('2d');
let tick = 0, W = 0, H = 0, dpr = 1, ch = 0, img = null, seen = true, raf = 0, last = 0, prev = 0;
let SX = 0, SY = 0, SR = 1; // 羊水囊圆心/半径(CSS px),焦散在囊附近更亮
let SX = 0, SY = 0, SR = 1; // 孕囊圆心/半径(CSS px)
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));
ch = Math.max(8, Math.round(CW * H / W));
off.width = CW; off.height = ch; img = oc.createImageData(CW, ch);
SX = b.left - r.left + b.width / 2; SY = b.top - r.top + b.height / 2; SR = Math.max(b.width, 60) / 2;
}
// 焦散:真实水底那种不规则细亮网 = 一群慢慢游动的点的「细胞边界」(到最近两点距离差 ≈ 0 的地方最亮),
// 坐标再加一点正弦扭曲让边线弯起来;大小两层叠加,一层快一层慢,网格才会像水一样不停变形
function mkLayer(n, cell, seed) {
let r = seed;
const rnd = () => ((r = (r * 16807) % 2147483647) / 2147483647);
return { cell, pts: Array.from({ length: n }, () => [rnd(), rnd(), rnd() * 6.28, .35 + rnd() * .5]) };
// 宝宝保护:孕囊中心附近 → 0,囊边 → 1
function guard(x, y) {
const d = Math.hypot(x - SX, y - SY) / SR;
return d < .45 ? 0 : d > .95 ? 1 : (d - .45) / .5;
}
const LAYERS = [[mkLayer(26, 22, 11), .8, 1], [mkLayer(14, 32, 29), .5, .8]];
const PX = new Float32Array(40), PY = new Float32Array(40);
function caustics(t) {
const d = img.data, sx = SX / W * CW, sy = SY / H * ch, sr = SR / W * CW;
const acc = caustics.acc && caustics.acc.length === CW * ch ? caustics.acc : (caustics.acc = new Float32Array(CW * ch));
acc.fill(0);
const k = CW / 120;
for (const [L, gain, speed] of LAYERS) {
const tt = t * speed, cell = L.cell * k, sg = 1 / (2 * (cell * .06) ** 2), n = L.pts.length;
for (let i = 0; i < n; i++) {
const [bx, by, ph, sp] = L.pts[i];
PX[i] = bx * CW + Math.cos(tt * sp + ph) * cell * .35;
PY[i] = by * ch + Math.sin(tt * sp * 1.3 + ph) * cell * .35;
}
for (let y = 0; y < ch; y++) {
const ox = Math.sin(y * .11 / k + tt * .7) * 2.2 * k;
for (let x = 0; x < CW; x++) {
const wx = x + ox, wy = y + Math.sin(x * .09 / k - tt * .6) * 2.2 * k;
let m1 = 1e9, m2 = 1e9;
for (let i = 0; i < n; i++) {
const dx = wx - PX[i], dy = wy - PY[i], q = dx * dx + dy * dy;
if (q < m1) { m2 = m1; m1 = q; } else if (q < m2) m2 = q;
}
const e = Math.sqrt(m2) - Math.sqrt(m1);
acc[y * CW + x] += Math.exp(-e * e * sg) * gain;
}
}
}
// 水波:四层不同方向、不同速度的长波叠加,取正半部分 → 一片片柔和的亮斑缓缓流过
function water(t) {
const d = img.data, T = t * .45;
for (let y = 0; y < ch; y++) {
for (let x = 0; x < CW; x++) {
let v = acc[y * CW + x]; v = v >= 1 ? 1 : Math.pow(v, 1.3);
// 囊里亮、囊外淡,上半部比下半部亮(光从上面来)
const dd = Math.hypot(x - sx, y - sy) / sr;
const w = (.3 + .7 * Math.max(0, 1 - Math.max(0, dd - .6) * 1.8)) * (1 - y / ch * .45);
const u = x / CW * 6, v = y / CW * 6;
let w = Math.sin(u * 1.1 + T) + Math.sin(v * 1.3 - T * .8 + u * .4) + Math.sin((u - v) * .9 + T * .6) + Math.sin(Math.hypot(u - 4.5, v - 1) * 1.6 - T * 1.1);
w = w / 4; w = w > 0 ? w * w : 0; // 只留亮的那半,平方让过渡更软
const X = x / CW * W, Y = y / ch * H;
const g = .15 + .85 * guard(X, Y);
const o = (y * CW + x) * 4;
d[o] = 255; d[o + 1] = 240; d[o + 2] = 220; d[o + 3] = v * 255 * w;
d[o] = 255; d[o + 1] = 238; d[o + 2] = 214; d[o + 3] = w * 200 * g * (1 - y / ch * .35);
}
}
oc.putImageData(img, 0, 0);
}
function drawCaustics() {
function drawWater() {
ctx.globalCompositeOperation = 'lighter';
ctx.imageSmoothingEnabled = true;
ctx.globalAlpha = 1;
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
ctx.globalAlpha = .55;
ctx.drawImage(off, 0, 0, W, H);
ctx.globalAlpha = 1;
}
// 光束:光源在头图上方偏右的画外,斜着往左下打;每道画三层(宽/中/窄)叠出软边
const RAYS = [[-.16, .030, .7], [-.07, .018, .9], [.01, .040, .6], [.09, .022, 1], [.17, .032, .7], [.26, .02, .55]];
let rayA = [];
// 柔光束:光源在头图上方偏右画外;每道是一条很宽的渐变扇形,画在小画布上再放大
const RAYS = [[-.12, .07, .8], [0, .05, .7], [.11, .08, 1], [.23, .06, .7]];
function rays(t) {
const ox = W * .78, oy = -H * .9, L = H * 2.6;
ctx.globalCompositeOperation = 'lighter';
rayA = [];
RAYS.forEach(([a0, w, al], i) => {
const a = Math.PI * .58 + a0 + Math.sin(t * .11 + i * 1.9) * .025;
const k = al * (.55 + .45 * Math.sin(t * .29 + i * 2.1)) * (.85 + .15 * Math.sin(t * 1.3 + i * 3.7));
rayA.push([a, w, k]);
[[2.2, .25], [1.3, .5], [.6, 1]].forEach(([wm, am]) => {
const ww = w * wm;
const gr = ctx.createLinearGradient(ox, oy, ox + Math.cos(a) * L * .75, oy + Math.sin(a) * L * .75);
gr.addColorStop(0, 'rgba(255,250,230,0)');
gr.addColorStop(.36, 'rgba(255,248,226,' + (.42 * k * am).toFixed(3) + ')');
gr.addColorStop(.62, 'rgba(255,242,215,' + (.22 * k * am).toFixed(3) + ')');
gr.addColorStop(1, 'rgba(255,232,200,0)');
ctx.fillStyle = gr;
ctx.beginPath(); ctx.moveTo(ox, oy);
ctx.lineTo(ox + Math.cos(a - ww) * L, oy + Math.sin(a - ww) * L);
ctx.lineTo(ox + Math.cos(a + ww) * L, oy + Math.sin(a + ww) * L);
ctx.closePath(); ctx.fill();
});
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) => {
const a = Math.PI * .6 + a0 + Math.sin(t * .09 + i * 1.9) * .03;
const k = al * (.6 + .4 * Math.sin(t * .23 + 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,246,224,' + (.34 * k).toFixed(3) + ')');
gr.addColorStop(.65, 'rgba(255,240,215,' + (.15 * 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();
});
return [ox, oy];
// 宝宝区域光束减半:在小画布上挖一个软边的「淡区」
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);
}
// 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;落在光束里更亮
const DOTS = Array.from({ length: 34 }, () => ({ x: Math.random(), y: Math.random(), r: .5 + Math.random() * 1.4,
// 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;孕囊中心附近不画,免得落在宝宝脸上
const DOTS = Array.from({ length: 26 }, () => ({ x: Math.random(), y: Math.random(), r: .5 + Math.random() * 1.2,
vx: (Math.random() - .5) * .006, vy: -(.004 + Math.random() * .008), ph: Math.random() * 6.3, sp: .6 + Math.random() * 1.4 }));
function dots(t, dt, o) {
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;
let lit = 0;
const ang = Math.atan2(Y - o[1], X - o[0]);
for (const [a, w, k] of rayA) { const d = Math.abs(ang - a); if (d < w * 1.3) lit = Math.max(lit, k * (1 - d / (w * 1.3))); }
const al = (.18 + .3 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * (1 + lit * 1.6);
const X = p.x * W, Y = p.y * H, gd = guard(X, Y);
if (gd <= 0) continue;
const al = (.15 + .25 * (.5 + .5 * Math.sin(t * p.sp + p.ph))) * gd;
const R = p.r * 2.4, g = ctx.createRadialGradient(X, Y, 0, X, Y, R);
g.addColorStop(0, 'rgba(255,250,238,' + Math.min(al, .9).toFixed(3) + ')');
g.addColorStop(0, 'rgba(255,250,238,' + al.toFixed(3) + ')');
g.addColorStop(1, 'rgba(255,250,238,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(X, Y, R, 0, 6.2832); ctx.fill();
}
@@ -205,15 +171,15 @@
const t = now / 1000;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, W, H);
if ((tick ^= 1)) caustics(t); // 焦散 15fps 算一次就够(水底光网本来就慢),每帧照画
drawCaustics();
const o = rays(t);
dots(t, dt, o);
if ((tick ^= 1)) water(t); // 水波 15fps 算一次就够,每帧照画
drawWater();
rays(t);
dots(t, dt);
}
function kick() { if (!raf) raf = requestAnimationFrame(frame); }
size();
addEventListener('resize', () => { size(); kick(); });
box.addEventListener('transitionend', size); // 换阶段后孕囊尺寸变了,焦散的「囊附近更亮」跟着挪
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);