diff --git a/index.html b/index.html index 0349e44..1f494a6 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ 兜爸心选 · 孕育陪伴 - + diff --git a/js/app.js b/js/app.js index fb6956c..8fbfc7f 100644 --- a/js/app.js +++ b/js/app.js @@ -3,7 +3,7 @@ (function () { 'use strict'; /* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */ - var VER = '26100408'; + var VER = '26100409'; const $ = (s, c) => (c || document).querySelector(s); const $$ = (s, c) => Array.from((c || document).querySelectorAll(s)); diff --git a/js/hero-fx.js b/js/hero-fx.js index aec710b..00e520a 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'; @@ -59,77 +59,161 @@ if (force != null || window.__mbPreg) onPreg(window.__mbPreg); else setTimeout(() => { if (cur < 0) show(DEFAULT); }, 2500); - /* ---------------- 阳光 + 水波 ---------------- */ + /* ---------------- 阳光 + 水波 ---------------- + 2026-10-04 二版(用户:「水波纹上面要有光束打进来,水波纹要模拟真实羊水的感觉」): + · 折射:.hero-baby(羊水囊 + 宝宝)挂一个 SVG 位移滤镜,噪声图慢慢变形 → 隔着一层液体看的那种轻微晃动 + · 焦散:水底那种不规则、不停变形的细亮网(游动点的细胞边界 + 正弦扭曲),低分辨率算完放大,囊附近更亮 + · 光束:从顶上斜着打进来,三层叠出软边,各自慢慢摆、忽明忽暗,越往下越散 + · 悬浮微粒:羊水里的小颗粒,慢慢漂、闪一下,经过光束时更亮 */ const cv = document.getElementById('heroFx'); if (!cv || reduce) return; const hero = cv.parentNode, home = document.getElementById('screen-home'); const ctx = cv.getContext('2d'); - const CW = 72; // 焦散低分辨率宽 + + // 折射滤镜: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 off = document.createElement('canvas'), oc = off.getContext('2d'); - let W = 0, H = 0, dpr = 1, ch = 0, img = null, seen = true, raf = 0, last = 0; + 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),焦散在囊附近更亮 function size() { - const r = hero.getBoundingClientRect(); + 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); 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; } - // 光束:从右上角画外斜着照进来,每道各自慢慢摆、忽明忽暗 - const RAYS = [[-.10, .07, .55], [.02, .045, .4], [.12, .09, .5], [.24, .05, .35], [.34, .07, .3]]; - function rays(t) { - const ox = W * .92, oy = -H * .55, L = H * 2.1; - ctx.globalCompositeOperation = 'lighter'; - RAYS.forEach(([a0, w, al], i) => { - const a = Math.PI * .62 + a0 + Math.sin(t * .13 + i * 1.7) * .035; - const ww = w * (1 + .25 * Math.sin(t * .21 + i)); - const k = al * (.65 + .35 * Math.sin(t * .37 + i * 2.3)); - const x1 = ox + Math.cos(a - ww) * L, y1 = oy + Math.sin(a - ww) * L; - const x2 = ox + Math.cos(a + ww) * L, y2 = oy + Math.sin(a + ww) * L; - const gr = ctx.createLinearGradient(ox, oy, ox + Math.cos(a) * L * .8, oy + Math.sin(a) * L * .8); - gr.addColorStop(0, 'rgba(255,248,225,0)'); - gr.addColorStop(.3, 'rgba(255,244,215,' + (.16 * k).toFixed(3) + ')'); - gr.addColorStop(.62, 'rgba(255,236,205,' + (.07 * k).toFixed(3) + ')'); - gr.addColorStop(1, 'rgba(255,230,200,0)'); - ctx.fillStyle = gr; - ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath(); ctx.fill(); - }); + // 焦散:真实水底那种不规则细亮网 = 一群慢慢游动的点的「细胞边界」(到最近两点距离差 ≈ 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 的那条线」做亮线,越往下越淡 + 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, cx = CW * .7, cy = ch * .1; + 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; + } + } + } for (let y = 0; y < ch; y++) { - const fall = 1 - y / ch * .55; for (let x = 0; x < CW; x++) { - const v = Math.sin(x * .23 + t * .8) + Math.sin(y * .31 - t * .6 + x * .05) - + Math.sin((x + y) * .14 + t * .45) + Math.sin(Math.hypot(x - cx, y - cy) * .27 - t * 1.05); - let c = 1 - Math.abs(Math.sin(v * 1.15)); - c = c * c; c = c * c * c; // 六次方:只留细细的亮网 + 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 o = (y * CW + x) * 4; - d[o] = 255; d[o + 1] = 242; d[o + 2] = 222; d[o + 3] = c * 120 * fall; + d[o] = 255; d[o + 1] = 240; d[o + 2] = 220; d[o + 3] = v * 255 * w; } } oc.putImageData(img, 0, 0); + } + function drawCaustics() { ctx.globalCompositeOperation = 'lighter'; ctx.imageSmoothingEnabled = true; - ctx.globalAlpha = .55; + ctx.globalAlpha = 1; 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 = []; + 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(); + }); + }); + return [ox, oy]; + } + // 悬浮微粒:慢慢往上漂、左右游,忽明忽暗;落在光束里更亮 + const DOTS = Array.from({ length: 34 }, () => ({ x: Math.random(), y: Math.random(), r: .5 + Math.random() * 1.4, + 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) { + 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 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(1, 'rgba(255,250,238,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')))) return; + if (!(seen && !document.hidden && (!home || home.classList.contains('is-active')))) { prev = 0; return; } raf = requestAnimationFrame(frame); if (now - last < 33) return; // ≈30fps 足够,省电 - last = now; + 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); - caustics(t); rays(t); + if ((tick ^= 1)) caustics(t); // 焦散 15fps 算一次就够(水底光网本来就慢),每帧照画 + drawCaustics(); + const o = rays(t); + dots(t, dt, o); } 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); diff --git a/mama-box.css b/mama-box.css index 4916def..7417504 100644 --- a/mama-box.css +++ b/mama-box.css @@ -309,6 +309,8 @@ 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}