feat(首页彩蛋): 宝宝胎动按孕周分阶段,贴近真实 (v26100403)
- <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 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<title>兜爸心选 · 孕育陪伴</title>
|
<title>兜爸心选 · 孕育陪伴</title>
|
||||||
<!-- V:26100402 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
<!-- V:26100403 缓存版本标记:改 css/js 时同步 bump 该值(与 js/app.js 的 VER 同源),静态资源按 mtime 走 ETag 自动刷新 -->
|
||||||
<link rel="stylesheet" href="mama-box.css">
|
<link rel="stylesheet" href="mama-box.css">
|
||||||
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
|
||||||
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
|
<!-- 共享助手:授权/跳商城/位置上报(分享卡片同步,见 docs/h5-auth-standard.md 第四节) -->
|
||||||
|
|||||||
13
js/app.js
13
js/app.js
@@ -3,7 +3,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
/* 页面版本号(YYMMDD+序号):显示在「关于兜爸心选」页底部;bump 时同步 head 注释与 ?v= */
|
||||||
var VER = '26100402';
|
var VER = '26100403';
|
||||||
const $ = (s, c) => (c || document).querySelector(s);
|
const $ = (s, c) => (c || document).querySelector(s);
|
||||||
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
const $$ = (s, c) => Array.from((c || document).querySelectorAll(s));
|
||||||
|
|
||||||
@@ -1156,7 +1156,16 @@
|
|||||||
el.textContent = t || '';
|
el.textContent = t || '';
|
||||||
el.classList.toggle('hide', !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() {
|
function syncPregnancy() {
|
||||||
|
Promise.resolve().then(pubPreg);
|
||||||
/* 孕期小知识:只认真实孕周(自己判显隐,见 renderTips) */
|
/* 孕期小知识:只认真实孕周(自己判显隐,见 renderTips) */
|
||||||
renderTips();
|
renderTips();
|
||||||
nurseryRender();
|
nurseryRender();
|
||||||
@@ -1571,7 +1580,7 @@
|
|||||||
] },
|
] },
|
||||||
/* 2026-10-03 首页彩蛋开关(js/bump.js):第 5 项是 localStorage 键,开关状态真的存下来,不是摆设 */
|
/* 2026-10-03 首页彩蛋开关(js/bump.js):第 5 项是 localStorage 键,开关状态真的存下来,不是摆设 */
|
||||||
{ t: '首页小彩蛋', rows: [
|
{ t: '首页小彩蛋', rows: [
|
||||||
['sw', '宝宝在肚皮下动', '首页偶尔被宝宝顶起来,摁一下会躲开', 1, 'mb_bump']
|
['sw', '宝宝在肚皮下动', '孕 20 周起,首页偶尔被宝宝顶起来,摁一下会躲开', 1, 'mb_bump']
|
||||||
] },
|
] },
|
||||||
{ t: '', rows: [
|
{ t: '', rows: [
|
||||||
['txt', '我们不会把你的孕期数据用于广告投放,也不会卖给第三方。数据加密存储,你可以随时导出或删除。']
|
['txt', '我们不会把你的孕期数据用于广告投放,也不会卖给第三方。数据加密存储,你可以随时导出或删除。']
|
||||||
|
|||||||
68
js/bump.js
68
js/bump.js
@@ -20,6 +20,12 @@
|
|||||||
松手 窝 duang duang 回弹,周围的内容跟着颤几下
|
松手 窝 duang duang 回弹,周围的内容跟着颤几下
|
||||||
600ms 三次里有一次:旁边轻轻反踢一下(宝宝不服气)
|
600ms 三次里有一次:旁边轻轻反踢一下(宝宝不服气)
|
||||||
⚠️ iOS 的网页没有震动接口(微信 JS-SDK 也没有),苹果用户只有画面 + 声音。
|
⚠️ 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' = 关);系统开了「减弱动态效果」也不出来。 */
|
开关:「我的 → 隐私权限 → 宝宝在肚皮下动」,存 localStorage 'mb_bump'('0' = 关);系统开了「减弱动态效果」也不出来。 */
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -58,10 +64,34 @@
|
|||||||
fist: { rx: 44, ry: 44, sharp: 1.9, amp: 8, dur: [3, 3.8] },
|
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] },
|
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 },
|
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 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 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 },
|
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 };
|
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;
|
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;
|
const idle = () => performance.now() - lastTouch > 1500;
|
||||||
|
|
||||||
function appear(key, at) {
|
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);
|
const s = at || pickSpot(P);
|
||||||
if (!s) { wait = 1.5; return; }
|
if (!s) { wait = 1.5; return; }
|
||||||
if (!key) seq++;
|
if (!key) seq++;
|
||||||
@@ -170,8 +202,13 @@
|
|||||||
ft.ang = (Math.random() - .5) * 1.4;
|
ft.ang = (Math.random() - .5) * 1.4;
|
||||||
ft.rollDir = s.x > scr.clientWidth / 2 ? -1 : 1;
|
ft.rollDir = s.x > scr.clientWidth / 2 ? -1 : 1;
|
||||||
ft.spread.v = ft.spread.to = 1; ft.spread.vel = 0;
|
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.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;
|
ft.catchable = P !== PARTS.kick;
|
||||||
if (!skin) skin = findSkin(ft.x, ft.y);
|
if (!skin) skin = findSkin(ft.x, ft.y);
|
||||||
gather(ft.x, ft.y, Math.max(P.rx, P.ry) * 1.25 + 20);
|
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; }
|
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 };
|
ft.flee = { dx: dx / l, dy: dy / l, t: 0 };
|
||||||
hide(true); ft.h.k = 260; ft.h.c = 26;
|
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);
|
setTimeout(() => heart(fx + ft.flee.dx * 18, fy + ft.flee.dy * 18 - 10), 80);
|
||||||
// 三次里有一次:0.6 秒后在旁边轻轻反踢一下
|
// 三次里有一次:0.6 秒后在旁边轻轻反踢一下
|
||||||
clearTimeout(kickTimer);
|
clearTimeout(kickTimer);
|
||||||
if (Math.random() < .34) kickTimer = setTimeout(() => {
|
if (stage && Math.random() < stage.again) kickTimer = setTimeout(() => {
|
||||||
if (!active()) return;
|
if (!active()) return;
|
||||||
const k = { x: fx + ft.flee.dx * 55, y: fy + ft.flee.dy * 55 };
|
const k = { x: fx + ft.flee.dx * 55, y: fy + ft.flee.dy * 55 };
|
||||||
const r0 = scrRect();
|
const r0 = scrRect();
|
||||||
if (!hitOK(r0.left + k.x, r0.top + k.y - scr.scrollTop)) return;
|
if (!hitOK(r0.left + k.x, r0.top + k.y - scr.scrollTop)) return;
|
||||||
buzz(10); blip(170, 90, .07, .12);
|
buzz(10); blip(170, 90, .07, .12);
|
||||||
appear('kick', k); wait = 4 + Math.random() * 3;
|
appear('kick', k); wait = gap();
|
||||||
}, 600);
|
}, 600);
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
@@ -278,14 +315,21 @@
|
|||||||
const u = ft.life, P = ft.part;
|
const u = ft.life, P = ft.part;
|
||||||
if (P.roll) { // 小屁股:慢慢拱着往一边挪
|
if (P.roll) { // 小屁股:慢慢拱着往一边挪
|
||||||
const k = Math.min(u / ft.dur, 1);
|
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 {
|
} else {
|
||||||
ft.wob = Math.sin(u * 2.1) * .12;
|
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);
|
if (u > ft.dur) hide(false);
|
||||||
} else if (mode === 'hide') {
|
} 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') {
|
} else if (mode === 'flee') {
|
||||||
const f = ft.flee; f.t += dt;
|
const f = ft.flee; f.t += dt;
|
||||||
const s = 24 * (1 - Math.exp(-f.t * 7));
|
const s = 24 * (1 - Math.exp(-f.t * 7));
|
||||||
@@ -297,7 +341,7 @@
|
|||||||
if (Math.abs(top - lastTop) > 1.5) { still = 0; lastTop = top; }
|
if (Math.abs(top - lastTop) > 1.5) { still = 0; lastTop = top; }
|
||||||
else if (still < .4 && (still += dt) >= .4) {
|
else if (still < .4 && (still += dt) >= .4) {
|
||||||
const fy = ft.y + ft.oy - top, seen = fy > 30 && fy < scr.clientHeight - 30;
|
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 === '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);
|
if (mode === 'wait') wait = Math.min(wait, 1 + Math.random() * .6);
|
||||||
}
|
}
|
||||||
@@ -427,6 +471,6 @@
|
|||||||
else { wait = 1.2; tick(); }
|
else { wait = 1.2; tick(); }
|
||||||
});
|
});
|
||||||
// 调试 / 截图用:URL 带 bumpdebug 才挂
|
// 调试 / 截图用: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();
|
wait = 3; tick();
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user