From 6c656ecc28e6b0ad4bb9a167a480e8867b4436aa Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Sun, 4 Oct 2026 02:48:00 +0000 Subject: [PATCH] =?UTF-8?q?style(=E9=A6=96=E9=A1=B5):=20=E5=A4=B4=E5=9B=BE?= =?UTF-8?q?=E6=9F=94=E5=85=89=E4=B8=8E=E6=9F=94=E5=92=8C=E6=B0=B4=E6=B3=A2?= =?UTF-8?q?=E3=80=81=E5=AE=9D=E5=AE=9D=E4=B8=8D=E5=86=8D=E5=BD=A2=E5=8F=98?= =?UTF-8?q?=EF=BC=9B=E8=83=8E=E5=8A=A8=E8=B8=A2=E5=87=BA=E6=98=8E=E6=98=BE?= =?UTF-8?q?=E8=A4=B6=E7=9A=B1=E3=80=81=E5=8A=A0=E5=BF=AB=E8=BF=9E=E8=B8=A2?= =?UTF-8?q?=20(v26100410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 光束画在 1/6 小画布再放大,宽而柔;水波改为低分辨率正弦大片光影,去掉白色网状焦散 - 去掉孕囊 SVG 位移滤镜;孕囊中心区域水波/光束/微粒避开,宝宝脸不受影响 - 胎动:鼓包外放射状褶皱(左上亮右下暗、三层虚边),每一拍重新踢出、更深 - 连踢加「噔噔噔」快节奏(.22~.26s),26~33 周出现概率 40%→50% Co-Authored-By: Claude Opus 5.5 --- index.html | 2 +- js/app.js | 2 +- js/bump.js | 56 +++++++++++++++-- js/hero-fx.js | 168 ++++++++++++++++++++------------------------------ mama-box.css | 2 - 5 files changed, 120 insertions(+), 110 deletions(-) diff --git a/index.html b/index.html index 1f494a6..4bf3d8e 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ 兜爸心选 · 孕育陪伴 - + diff --git a/js/app.js b/js/app.js index 8fbfc7f..0df4e37 100644 --- a/js/app.js +++ b/js/app.js @@ -3,7 +3,7 @@ (function () { 'use strict'; /* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */ - var VER = '26100409'; + var VER = '26100410'; const $ = (s, c) => (c || document).querySelector(s); const $$ = (s, c) => Array.from((c || document).querySelectorAll(s)); diff --git a/js/bump.js b/js/bump.js index 9eb953f..e5b3819 100644 --- a/js/bump.js +++ b/js/bump.js @@ -81,11 +81,12 @@ const STAGES = { // drum:原地带节奏连踢的概率;chain:踢完不歇、旁边紧接着再踢的概率,最多连 chainMax 下(2026-10-04 用户:「有时会带点节奏、可以连续踢」) flutter: { order: ['flick', 'flick', 'tap', 'flick'], gap: [10, 18], kick: false, again: 0, double: .45, follow: false, drum: 0, chain: 0 }, - active: { order: ORDER, gap: [4, 8], kick: true, again: .34, double: 0, follow: true, drum: .4, chain: .3, chainMax: 3 }, + active: { order: ORDER, gap: [4, 8], kick: true, again: .34, double: 0, follow: true, drum: .5, chain: .3, chainMax: 3 }, late: { order: ['roll', 'butt', 'foot', 'roll', 'knee'], gap: [7, 12], kick: false, again: .15, double: 0, follow: true, slow: true, drum: .22, chain: .15, chainMax: 2 } }; // 原地连踢的节奏型(每拍间隔,秒):匀速「哒哒哒」、「哒哒—哒」、「哒—哒哒」、两下一组 - const RHYTHMS = [[.42, .42, .42], [.3, .3, .75, .3], [.6, .28, .28], [.32, .9, .32], [.38, .38, .38, .38]]; + // 2026-10-04 加了「噔噔噔」快连踢(.22~.26 秒一下),出现机会最大 + const RHYTHMS = [[.24, .24, .24], [.22, .22, .22, .22], [.26, .26, .26], [.42, .42, .42], [.3, .3, .75, .3], [.6, .28, .28], [.38, .38, .38, .38]]; const dbgWeek = (location.href.match(/bumpweek=(\d+)/) || [])[1]; let week = dbgWeek ? +dbgWeek : (window.__mbPreg ? window.__mbPreg.week : null); const stageOf = w => w == null || w < 20 ? null : w < 26 ? STAGES.flutter : w < 34 ? STAGES.active : STAGES.late; @@ -224,7 +225,7 @@ const inChain = ft.chained || ft.chainLeft > 0; if (inChain && !P.roll) ft.dur = (1.1 + Math.random() * .6) * (stage && stage.slow ? 1.3 : 1); // 原地连踢:脚丫 / 拳头 / 膝盖才有;晚期节奏放慢 1.4 倍。拍子排在第一下顶起来之后,整段时长跟着拉长 - ft.beats = null; ft.beat = 0; ft.pop = 0; + ft.beats = null; ft.beat = 0; ft.pop = 0; ft.wp = 0; ft.wseed = Math.random() * 1000; if (!inChain && stage && stage.drum && !P.roll && !P.soft && P !== PARTS.kick && Math.random() < stage.drum) { const r = RHYTHMS[Math.random() * RHYTHMS.length | 0], m = stage.slow ? 1.4 : 1; let t = .7; ft.beats = r.map(d => (t += d * m)); @@ -329,6 +330,7 @@ /* ---------- 物理 ---------- */ function step(dt) { + if (ft.wp > 0) ft.wp = Math.max(0, ft.wp - dt * 1.4); if (mode === 'wait') { if (active() && idle()) { wait -= dt; if (wait <= 0) appear(); } } else if (mode === 'show') { @@ -338,14 +340,15 @@ const k = Math.min(u / ft.dur, 1); ft.ox = ft.rollDir * (P.slide || 30) * k; ft.oy = -6 * Math.sin(k * Math.PI); ft.wob = Math.sin(u * 1.2) * .08; } else { + if (u < .5 && ft.wp < .9) ft.wp = Math.max(ft.wp, u * 2); // 第一下顶起来时褶皱一起出 ft.wob = Math.sin(u * 2.1) * .12; - if (u > ft.dur * .4 && !ft.kicked && P !== PARTS.kick && !P.soft && stage && stage.kick) { ft.kicked = true; ft.h.vel += 70; } // 中途再顶一下 + if (u > ft.dur * .4 && !ft.kicked && P !== PARTS.kick && !P.soft && stage && stage.kick) { ft.kicked = true; ft.h.vel += 70; ft.wp = 1; } // 中途再顶一下 // 一拍:先往回缩一下(鼓包塌一点),0.12 秒后再顶上来,位置跟着微微挪(脚尖 / 脚跟轮流使劲) if (ft.beats && ft.beat < ft.beats.length && u >= ft.beats[ft.beat]) { ft.beat++; ft.h.vel -= 85; ft.pop = .12; ft.ox += (Math.random() - .5) * 10; ft.oy += (Math.random() - .5) * 10; } - if (ft.pop > 0 && (ft.pop -= dt) <= 0) ft.h.vel += stage && stage.slow ? 95 : 150; + if (ft.pop > 0 && (ft.pop -= dt) <= 0) { ft.h.vel += stage && stage.slow ? 95 : 150; ft.wp = 1; ft.wseed += 7.3; } // 每一拍褶皱重新被踢出来 } if (u > ft.dur) { // 连续踢:这一下收得快一点,旁边紧接着再来,不等下一个间隔 @@ -453,6 +456,47 @@ } blob(x, y, rx, ry, ang, dark.concat(lite)); } + // 褶皱:像从背面顶一层绷紧的膜 —— 从鼓包边缘向外放射出几道弯弯的褶,每道 = 一条暗线 + 旁边错开一点的亮线, + // 越往外越细越淡。k = 鼓得多高,pulse = 刚被踢那一下(连踢时每一拍都重新踢出来、更深一点) + function rnd(n) { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); } + function wrinkles(x, y, rx, ry, ang, k, pulse, seed) { + if (k < .08) return; + const n = 8, R0 = Math.max(rx, ry); + cx.save(); cx.translate(x, y); cx.rotate(ang); cx.lineCap = 'round'; + for (let i = 0; i < n; i++) { + const a = (i + rnd(seed + i) * .6) / n * Math.PI * 2; + const len = (.5 + rnd(seed + i * 3.1) * .6) * (1 + pulse * .35); + const bend = (rnd(seed + i * 5.7) - .5) * .5; + const ca = Math.cos(a), sa = Math.sin(a); + // 起点贴着鼓包椭圆边,往外走 + const sx0 = ca * rx * .7, sy0 = sa * ry * .7; + const ex = ca * (rx * .7 + R0 * len), ey = sa * (ry * .7 + R0 * len); + const mx = (sx0 + ex) / 2 - sa * R0 * bend * len, my = (sy0 + ey) / 2 + ca * R0 * bend * len; + const al = Math.min(1, k) * (.55 + .45 * pulse) * (.7 + .3 * rnd(seed + i * 9.2)); + // 垂直于褶的方向;统一让「朝左上」的那一侧亮(光从左上来,和鼓包的高光一致),褶棱才立得起来 + let nx = -sa, ny = ca; + { const wa = ang, wx = nx * Math.cos(wa) - ny * Math.sin(wa), wy = nx * Math.sin(wa) + ny * Math.cos(wa); if (wx + wy > 0) { nx = -nx; ny = -ny; } } + // 每道褶 = 暗面 + 亮面两条宽软带,各用三层由宽到窄叠出虚边(canvas 的 filter:blur 在老 iOS 上不支持) + for (const [col, a1, off] of [['60,48,42', .16, -2.4], ['255,255,255', .36, 2.4]]) { + for (const [wm, am] of [[9, .45], [5.5, .7], [2.6, 1]]) { + const g = cx.createLinearGradient(sx0, sy0, ex, ey); + g.addColorStop(0, 'rgba(' + col + ',0)'); + g.addColorStop(.15, 'rgba(' + col + ',' + (a1 * am * al).toFixed(3) + ')'); + g.addColorStop(.6, 'rgba(' + col + ',' + (a1 * am * al * .5).toFixed(3) + ')'); + g.addColorStop(1, 'rgba(' + col + ',0)'); + cx.strokeStyle = g; cx.lineWidth = wm * (1 + pulse * .3); + const ox = nx * off, oy = ny * off; + cx.beginPath(); cx.moveTo(sx0 + ox, sy0 + oy); cx.quadraticCurveTo(mx + ox, my + oy, ex + ox, ey + oy); cx.stroke(); + } + } + } + // 鼓包一圈绷紧的边:一道很淡的暗环,让「膜被撑开」更明显 + cx.scale(rx, ry); + const g = cx.createRadialGradient(0, 0, .7, 0, 0, 1.15); + g.addColorStop(0, 'rgba(70,58,52,0)'); g.addColorStop(.5, 'rgba(70,58,52,' + (.12 * Math.min(1, k)).toFixed(3) + ')'); g.addColorStop(1, 'rgba(70,58,52,0)'); + cx.fillStyle = g; cx.beginPath(); cx.arc(0, 0, 1.15, 0, 7); cx.fill(); + cx.restore(); + } function paint() { cx.setTransform(dpr, 0, 0, dpr, 0, 0); cx.clearRect(0, 0, cv.width, cv.height); @@ -460,6 +504,8 @@ const H = ft.h.v; if (H > .1) { const P = ft.part, sp = ft.spread.v, k = Math.min(H / 9, 1.3) / sp; + // 2026-10-04 用户:「踢屏幕时前面要有比较明显的褶皱;连续踢就连续被踢出褶」—— 小鱼泡泡(早期)不出褶 + if (!P.soft) wrinkles(ft.x + ft.ox + ox, ft.y + ft.oy + oy, P.rx * sp, P.ry * sp, ft.ang + (mode === 'show' ? ft.wob : 0), Math.min(H / P.amp, 1.2), ft.wp || 0, ft.wseed || 0); blob(ft.x + ft.ox + ox, ft.y + ft.oy + oy, P.rx * sp, P.ry * sp, ft.ang + (mode === 'show' ? ft.wob : 0), [ // 2026-10-04 用户:「凸起就行,不要变颜色(之前踢完泛浅红)」—— 只留中性灰的背光阴影 + 白高光,不带任何暖色 [.16, .22, 1.02, 'rgba(70,62,58,' + (.1 * k).toFixed(3) + ')'], diff --git a/js/hero-fx.js b/js/hero-fx.js index 00e520a..f1f8db7 100644 --- a/js/hero-fx.js +++ b/js/hero-fx.js @@ -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 = '' + - '' + - '' + - ''; - 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); diff --git a/mama-box.css b/mama-box.css index 7417504..4916def 100644 --- a/mama-box.css +++ b/mama-box.css @@ -309,8 +309,6 @@ a.skip:focus{left:16px;top:16px} -webkit-mask-image:radial-gradient(closest-side,#000 84%,transparent 100%); mask-image:radial-gradient(closest-side,#000 84%,transparent 100%)} .hero-kid.is-on{opacity:1} -/* 液体折射(SVG 滤镜 #mbFluid 由 hero-fx.js 注入;减少动态效果时 JS 不加这个类) */ -.hero-baby.is-fluid{filter:url(#mbFluid)} /* 阳光 + 水波:整块橙区一张画布,加色混合,叠在宝宝上、文字下 */ .hero-fx{position:absolute;z-index:2;left:0;top:0;width:100%;height:100%;pointer-events:none;mix-blend-mode:screen;opacity:0;transition:opacity 1.2s ease} .hero-fx.is-on{opacity:1}