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();
})();