From f737d1f18db15753598ac7f4d3d9dd351581a2e4 Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Sun, 4 Oct 2026 00:36:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(=E9=A6=96=E9=A1=B5=E5=BD=A9=E8=9B=8B):=20?= =?UTF-8?q?=E5=AE=9D=E5=AE=9D=E8=83=8E=E5=8A=A8=E6=8C=89=E5=AD=95=E5=91=A8?= =?UTF-8?q?=E5=88=86=E9=98=B6=E6=AE=B5=EF=BC=8C=E8=B4=B4=E8=BF=91=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=20(v26100403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - <20 周 / 未建档 / 产后:不出现 - 20~25 周:小鱼吐泡泡(flick/tap),小浅快、常连两下,隔 10~25 秒,不追屏 - 26~33 周:完整版拳打脚踢,最勤 - 34 周起:多为大片慢拱(roll/butt),猛踢减少、节奏放慢 - app.js syncPregnancy 结束后广播 mb-preg;调试参数 bumpweek=N Co-Authored-By: Claude Opus 5.5 --- index.html | 2 +- js/app.js | 13 +++++++++-- js/bump.js | 68 ++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/index.html b/index.html index 998f551..05ce0db 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ 兜爸心选 · 孕育陪伴 - + diff --git a/js/app.js b/js/app.js index 8aabeb0..752ba50 100644 --- a/js/app.js +++ b/js/app.js @@ -3,7 +3,7 @@ (function () { 'use strict'; /* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */ - var VER = '26100402'; + var VER = '26100403'; const $ = (s, c) => (c || document).querySelector(s); const $$ = (s, c) => Array.from((c || document).querySelectorAll(s)); @@ -1156,7 +1156,16 @@ el.textContent = t || ''; el.classList.toggle('hide', !t); } + /* 把当前孕期状态广播给首页彩蛋 js/bump.js(宝宝在肚皮底下动按孕周分阶段,2026-10-04)。 + syncPregnancy 有好几个提前 return 的分支,所以用微任务等它整个跑完再发,拿到的是最终状态。 */ + function pubPreg() { + const d = { week: (state.postpartum || state.pregEmpty) ? null : state.week, day: state.day, + postpartum: !!state.postpartum, empty: !!state.pregEmpty }; + window.__mbPreg = d; + window.dispatchEvent(new CustomEvent('mb-preg', { detail: d })); + } function syncPregnancy() { + Promise.resolve().then(pubPreg); /* 孕期小知识:只认真实孕周(自己判显隐,见 renderTips) */ renderTips(); nurseryRender(); @@ -1571,7 +1580,7 @@ ] }, /* 2026-10-03 首页彩蛋开关(js/bump.js):第 5 项是 localStorage 键,开关状态真的存下来,不是摆设 */ { t: '首页小彩蛋', rows: [ - ['sw', '宝宝在肚皮下动', '首页偶尔被宝宝顶起来,摁一下会躲开', 1, 'mb_bump'] + ['sw', '宝宝在肚皮下动', '孕 20 周起,首页偶尔被宝宝顶起来,摁一下会躲开', 1, 'mb_bump'] ] }, { t: '', rows: [ ['txt', '我们不会把你的孕期数据用于广告投放,也不会卖给第三方。数据加密存储,你可以随时导出或删除。'] diff --git a/js/bump.js b/js/bump.js index 743463d..9be0a13 100644 --- a/js/bump.js +++ b/js/bump.js @@ -20,6 +20,12 @@ 松手 窝 duang duang 回弹,周围的内容跟着颤几下 600ms 三次里有一次:旁边轻轻反踢一下(宝宝不服气) ⚠️ iOS 的网页没有震动接口(微信 JS-SDK 也没有),苹果用户只有画面 + 声音。 + 按孕周分阶段,尽量贴近真实胎动(2026-10-04 用户:「尽量模拟真实的」): + · 20 周前 / 没填预产期 / 已产后:不出现(这时候肚皮外面看不到胎动;没孕周就不猜) + · 20~25 周「小鱼吐泡泡」:小、浅、一闪就没,常常连着两下;隔 10~25 秒才一次,周数越小越稀;滑过来也不追 + · 26~33 周「拳打脚踢」:完整版——小脚丫 / 小拳头 / 膝盖 / 小屁股,最勤(隔 4~8 秒),中途还会再顶一下 + · 34 周起「地方小了」:很少猛踢,多是一大片慢慢拱过去(翻身、屁股),节奏放慢(隔 7~12 秒),次数不减 + 孕周来自 app.js 的 mb-preg 事件(syncPregnancy 跑完后广播)。调试:URL 带 bumpweek=24 可强制按 24 周演示。 开关:「我的 → 隐私权限 → 宝宝在肚皮下动」,存 localStorage 'mb_bump'('0' = 关);系统开了「减弱动态效果」也不出来。 */ (function () { 'use strict'; @@ -58,10 +64,34 @@ fist: { rx: 44, ry: 44, sharp: 1.9, amp: 8, dur: [3, 3.8] }, knee: { rx: 48, ry: 80, sharp: 1.6, amp: 8.5, dur: [3.4, 4.2] }, butt: { rx: 92, ry: 78, sharp: 1.25, amp: 7, dur: [4, 5], roll: true }, - kick: { rx: 34, ry: 36, sharp: 1.8, amp: 4.5, dur: [.35, .45] } // 被摁后不服气的反踢:小、快、摁不到 + kick: { rx: 34, ry: 36, sharp: 1.8, amp: 4.5, dur: [.35, .45] }, // 被摁后不服气的反踢:小、快、摁不到 + // 20~25 周:宝宝还小,只有很轻的一下(flick),偶尔稍大一点的轻碰(tap) + flick: { rx: 32, ry: 34, sharp: 1.7, amp: 5, dur: [.8, 1.2], soft: true }, + tap: { rx: 42, ry: 42, sharp: 1.8, amp: 6.5, dur: [1.4, 2], soft: true }, + // 34 周起:一大片慢慢拱过去(翻身),长条形,边走边起伏 + roll: { rx: 118, ry: 72, sharp: 1.15, amp: 6.5, dur: [5.5, 7], roll: true, slide: 64 } }; const ORDER = ['foot', 'fist', 'butt', 'knee']; + /* ---------- 孕周 → 阶段 ---------- */ + const STAGES = { + flutter: { order: ['flick', 'flick', 'tap', 'flick'], gap: [10, 18], kick: false, again: 0, double: .45, follow: false }, + active: { order: ORDER, gap: [4, 8], kick: true, again: .34, double: 0, follow: true }, + late: { order: ['roll', 'butt', 'foot', 'roll', 'knee'], gap: [7, 12], kick: false, again: .15, double: 0, follow: true, slow: true } + }; + 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; + let stage = stageOf(week); + // 两次之间隔多久:20~22 周最稀(再 +40%),越往后越接近本阶段的基准 + const gap = () => { const g = stage ? stage.gap : [6, 10], m = week != null && week < 23 ? 1.4 : 1; return (g[0] + Math.random() * (g[1] - g[0])) * m; }; + addEventListener('mb-preg', e => { + if (dbgWeek) return; + week = e.detail ? e.detail.week : null; stage = stageOf(week); + if (!stage && mode !== 'wait') { mode = 'wait'; ft.h.v = pk.d.v = 0; release(); cx.clearRect(0, 0, cv.width, cv.height); } + }); + let near = null; // 「连着两下」时第二下的位置 + const spring = (o, dt) => { const a = -o.k * (o.v - o.to) - o.c * o.vel; o.vel += a * dt; o.v += o.vel * dt; }; const ft = { x: 0, y: 0, ang: 0, part: PARTS.foot, h: { v: 0, vel: 0, to: 0, k: 140, c: 11 }, spread: { v: 1, vel: 0, to: 1, k: 260, c: 18 }, life: 0, dur: 3, ox: 0, oy: 0, wob: 0 }; @@ -157,11 +187,13 @@ return null; } - const active = () => !isOff() && !document.hidden && scr.classList.contains('is-active'); + const active = () => !!stage && !isOff() && !document.hidden && scr.classList.contains('is-active'); const idle = () => performance.now() - lastTouch > 1500; function appear(key, at) { - const P = PARTS[key || ORDER[seq % ORDER.length]]; + const ord = stage ? stage.order : ORDER; + const P = PARTS[key || ord[seq % ord.length]]; + if (!at && near) { const r0 = scrRect(); if (hitOK(r0.left + near.x, r0.top + near.y - scr.scrollTop)) at = near; near = null; } const s = at || pickSpot(P); if (!s) { wait = 1.5; return; } if (!key) seq++; @@ -170,8 +202,13 @@ ft.ang = (Math.random() - .5) * 1.4; ft.rollDir = s.x > scr.clientWidth / 2 ? -1 : 1; ft.spread.v = ft.spread.to = 1; ft.spread.vel = 0; - ft.h.to = P.amp; ft.h.k = P.roll ? 70 : 140; ft.h.c = 9; ft.h.vel += P.roll ? 30 : 120; + // 晚期地方小:同样的部位也顶得慢、顶得闷;早期的小鱼泡泡快起快落 + const slow = stage && stage.slow && !P.roll; + ft.h.to = P.amp * (slow ? .8 : 1); + ft.h.k = P.roll ? 70 : slow ? 75 : P.soft ? 220 : 140; ft.h.c = P.soft ? 16 : 9; + ft.h.vel += P.roll ? 30 : slow ? 45 : P.soft ? 50 : 120; ft.life = 0; ft.dur = P.dur[0] + Math.random() * (P.dur[1] - P.dur[0]); ft.kicked = false; + ft.chained = !!ft.chainNext; ft.chainNext = false; ft.catchable = P !== PARTS.kick; if (!skin) skin = findSkin(ft.x, ft.y); gather(ft.x, ft.y, Math.max(P.rx, P.ry) * 1.25 + 20); @@ -226,17 +263,17 @@ if (l < 6) { dx = -Math.sin(ft.ang); dy = Math.cos(ft.ang); l = 1; } ft.flee = { dx: dx / l, dy: dy / l, t: 0 }; hide(true); ft.h.k = 260; ft.h.c = 26; - mode = 'flee'; wait = 4 + Math.random() * 3; + mode = 'flee'; wait = gap(); setTimeout(() => heart(fx + ft.flee.dx * 18, fy + ft.flee.dy * 18 - 10), 80); // 三次里有一次:0.6 秒后在旁边轻轻反踢一下 clearTimeout(kickTimer); - if (Math.random() < .34) kickTimer = setTimeout(() => { + if (stage && Math.random() < stage.again) kickTimer = setTimeout(() => { if (!active()) return; const k = { x: fx + ft.flee.dx * 55, y: fy + ft.flee.dy * 55 }; const r0 = scrRect(); if (!hitOK(r0.left + k.x, r0.top + k.y - scr.scrollTop)) return; buzz(10); blip(170, 90, .07, .12); - appear('kick', k); wait = 4 + Math.random() * 3; + appear('kick', k); wait = gap(); }, 600); run(); } @@ -278,14 +315,21 @@ const u = ft.life, P = ft.part; if (P.roll) { // 小屁股:慢慢拱着往一边挪 const k = Math.min(u / ft.dur, 1); - ft.ox = ft.rollDir * 30 * k; ft.oy = -6 * Math.sin(k * Math.PI); ft.wob = Math.sin(u * 1.2) * .08; + 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 { ft.wob = Math.sin(u * 2.1) * .12; - if (u > ft.dur * .4 && !ft.kicked && P !== PARTS.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; } // 中途再顶一下 } if (u > ft.dur) hide(false); } else if (mode === 'hide') { - if (ft.h.v < .3) { mode = 'wait'; wait = 4 + Math.random() * 4; } + if (ft.h.v < .3) { + mode = 'wait'; wait = gap(); + // 小鱼吐泡泡常常是连着的:一闪没了,旁边紧跟着又一下 + if (stage && stage.double && ft.part.soft && !ft.chained && Math.random() < stage.double) { + const a = Math.random() * Math.PI * 2; + near = { x: ft.x + Math.cos(a) * 42, y: ft.y + Math.sin(a) * 42 }; wait = .35 + Math.random() * .3; ft.chainNext = true; + } + } } else if (mode === 'flee') { const f = ft.flee; f.t += dt; const s = 24 * (1 - Math.exp(-f.t * 7)); @@ -297,7 +341,7 @@ if (Math.abs(top - lastTop) > 1.5) { still = 0; lastTop = top; } else if (still < .4 && (still += dt) >= .4) { const fy = ft.y + ft.oy - top, seen = fy > 30 && fy < scr.clientHeight - 30; - if (!seen) { + if (!seen && stage && stage.follow) { if (mode === 'show' || mode === 'hide') { ft.h.v = 0; ft.h.vel = 0; mode = 'wait'; release(); } if (mode === 'wait') wait = Math.min(wait, 1 + Math.random() * .6); } @@ -427,6 +471,6 @@ else { wait = 1.2; tick(); } }); // 调试 / 截图用:URL 带 bumpdebug 才挂 - if (/bumpdebug/.test(location.href)) window.__bump = { ft, pk, atoms: () => atoms.map(a => [a.el.className || a.el.tagName, Math.round(a.x), Math.round(a.y), a.el.style.transform.slice(0, 90)]), mode: () => mode, now: () => { wait = 0; lastTouch = 0; }, at: (k, x, y) => { lastTouch = 0; appear(k, { x, y: y + scr.scrollTop }); } }; + if (/bumpdebug/.test(location.href)) window.__bump = { ft, pk, atoms: () => atoms.map(a => [a.el.className || a.el.tagName, Math.round(a.x), Math.round(a.y), a.el.style.transform.slice(0, 90)]), mode: () => mode, now: () => { wait = 0; lastTouch = 0; }, week: () => week, at: (k, x, y) => { lastTouch = 0; appear(k, { x, y: y + scr.scrollTop }); } }; wait = 3; tick(); })();