Compare commits

...

241 Commits

Author SHA1 Message Date
48773baa42 合并 main(goHome 读后台首页配置)到 preview,准备作为正式版 /mama-box-v2/ (v26101001)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:22:28 +00:00
495c6a3dd2 AGENTS.md 硬约定 14:goHome 前先 rememberIdentity(身份存 localStorage 的应用)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:17:36 +00:00
ae8324467c AGENTS.md:首页地址改后台动态配置;新增硬约定 14 —— 返回首页一律 wxAuth.goHome()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:13:09 +00:00
c911f63ee5 AGENTS.md 增加硬约定 13:跳转/接口一律同源相对路径,mp 404 时补 nginx 反代
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:34:21 +00:00
835bd39a2d 档案/消消乐/婴儿房/兜豆商城入口去掉域名,改同源相对路径 (v26100906)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:28:26 +00:00
d6904f364a 档案/消消乐/婴儿房/兜豆商城入口改走 mp.yizhezhe.com 业务域名反代 (v26100905)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:21:48 +00:00
15c2fb7142 首页消消乐卡片改名「宝贝消消乐」 (v26100904)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:03:39 +00:00
ca4e22a729 首页消消乐入口改跳 taidong.douba.top/muying-xiaoxiao/,简介更新 (v26100903)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:04:04 +00:00
28fc5ff9aa 首页「我们的家」婴儿房入口改跳 taidong.douba.top/yinger-fang/ (v26100902)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:02:40 +00:00
65f1603082 星座详情:关闭按钮挪到右上角 (v26100901)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:00:16 +00:00
dc156b7a6a 预览版默认对接真实接口:工具卡、脚印、档案入口不再固定用示例,仅 demo=1 / fpdemo=1 时显示示例 (v26100465)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:51:02 +00:00
ce544f1d8a 盲盒直达链接:地址带 open=box 时自动弹出盲盒面板 (v26100464)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:26:06 +00:00
26bff164c3 首页婴儿房封面上方补天空:树尖补全、往上渐变成天空,滑到顶不再截断树 (v26100463)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:07:21 +00:00
323acc3dc3 首页兑换入口标题「兜豆兑换实物」改为「兜豆兑换」 (v26100462)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:57:11 +00:00
45bacdd1c2 我的资料:宝宝卡标题改为「宝宝(小名)」,去掉输入框上方的「小名」 (v26100461)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:56:45 +00:00
57e845eb2b 首页加夜晚模式:右上角月亮/太阳切换,样式由 tools/gen-night.py 自动翻色 (v26100460)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:48:54 +00:00
e9cfa10fca 预览站档案入口一律带 demo=1 显示示例档案 (v26100459)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:49:52 +00:00
4c2bba9dc5 档案入口带 return_url(成长档案会话要求)(v26100458)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:43:06 +00:00
4cb6a4a9b9 「记一下」面板渐变加深一点 (v26100457)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 05:15:29 +00:00
20317c94ab 「记一下」工具面板加淡渐变底 (v26100456)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:32:12 +00:00
62db1a1463 「我的」页孕期卡册入口隐藏 (v26100455)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:27:42 +00:00
f900c018bc 「我的」常用服务加兑换记录入口(跳商城 ?page=orders,角标显示待发货笔数)(v26100454)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:22:03 +00:00
6074193f7c 今日脚印去掉「全部 ›」(v26100453)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:12:13 +00:00
531e406f14 预览站示例模式 demo=1:孕29周、示例脚印、来信角标2、兜豆2000、档案带demo (v26100452)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:01:47 +00:00
f733855095 今日脚印掀角缩到 56px 不压待办卡,「祈福」改写在纸背正中顺折痕 (v26100451)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:58:14 +00:00
8f645186a1 今日脚印「全部」再让开放大后的掀角 (v26100450)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:52:30 +00:00
5f62d32ba2 今日脚印掀角放大露出整棵祈福树,纸背加「祈福」(v26100449)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:51:43 +00:00
14cd1e7ad1 今日脚印「全部」再往左让,避开掀角 (v26100448)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:42:10 +00:00
de68ae20d4 祈福树入口移到今日脚印右上角掀角;工具卡恢复「所有工具」(v26100447)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:41:03 +00:00
19525a6f68 宝宝来信角标改实心红圆;停在首页每 60 秒刷新未读数 (v26100446)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:26:51 +00:00
3caee8e94b 祈福小树下移,卡片上沿改为渐隐露出树身 (v26100445)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:20:54 +00:00
8993a86490 祈福小树:卡片下面那截裁掉,不再透过毛玻璃糊成一团 (v26100444)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:17:10 +00:00
bc4f5b38e6 祈福小树改为藏在工具卡后面、只从卡片上沿露出树冠 (v26100443)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:14:13 +00:00
e35218690e 祈福入口小树换成宝宝祈福提供的缩略图 (v26100442)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:08:14 +00:00
4c31cb1c17 首页毛玻璃底下的彩色光斑尺寸 ×0.7,减少花感 (v26100441)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:06:06 +00:00
86a5b608bd 工具卡右上角「所有工具」换成祈福树入口(小树探头+偶尔摆动,跳 /qifu-shu/)(v26100440)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:02:37 +00:00
b87afd1b27 首页宝宝时不时缓缓动一动小手小脚(WebGL 局部扭动,脸不动)(v26100439)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:04:27 +00:00
c957be3a77 星座只显示一个(去掉分界双星座);卡片位置按孕周分两档 (v26100438)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:02:00 +00:00
da4560ce0e 星座卡片移到孕囊下方,离白边约5px (v26100437)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:55:32 +00:00
3969aecdc0 首页头部文字统一纯白+深暖棕投影 (v26100436)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:51:24 +00:00
914c3e1bed 首页星座卡片按囊壁定位,只压孕囊右下边缘 (v26100435)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:46:23 +00:00
98a948db39 宝宝来信 tab 角标改圈数字①~⑩,>10 显示 10+;bfcache 返回也重拉未读 (v26100434)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:55:02 +00:00
2b15f78706 首页星座卡片再调透到 28% (v26100433)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:41:57 +00:00
7e1381f70c 首页星座卡片再调透 (v26100432)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:26:40 +00:00
89567e3649 首页星座卡片改半透明 (v26100431)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:24:15 +00:00
e3774522ce 首页星座牌改实体卡片,按火土风水四象上色 (v26100430)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:22:08 +00:00
6ab8c1fa17 首页头图:去掉见面倒计时前的白色小心 (v26100429)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 10:16:38 +00:00
26f6e90eaa 我的页:常用服务改一行横排 (v26100428)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:43:18 +00:00
080d0481e4 首页头图:换参考页 WebGL 羊水+光束,孕囊改透明底 PNG (v26100427)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:09:20 +00:00
4d412f0e60 feat(星座宝宝): 12 个自绘星座符号;弹窗按火土风水四象重做——头带配色、星图、元素纹样 (v26100426)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:53:54 +00:00
038207d585 feat(首页头图): 点星座小牌弹出星座宝宝——幸运色、性格、可能喜欢的职业,交界可切换 (v26100425)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:44:28 +00:00
d01a617ec5 feat(首页头图): 按预产期推算星座,孕囊右下角挂「xx宝宝」玻璃小牌 (v26100424)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:40:36 +00:00
62d885ab72 feat(我的): 意见反馈——功能问题/建议/其他 + 截图 + 联系方式,我提交过的 (v26100423)
后端 box_app 新增 /api/mp/public/mama-box/feedback(POST/GET)与管理端 /api/mama-box/feedbacks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:34:55 +00:00
72a7e4b935 style(首页头图): 白色水纹换成有深度的羊水层——背景折射晃动、暗流、景深暗角、远近两层微粒 (v26100422)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:12:45 +00:00
dca1b69eff style(首页头图): LOGO 回白字、倒计时去框;满幅羊水明暗流动,按宝宝轮廓遮罩挖空;孕囊轻浮 (v26100421)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:36:23 +00:00
0f73b85f10 feat(底栏): 「档案」tab 跳成长档案 v7 → v8 (v26100420)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:29:12 +00:00
55e293874f style(首页头图): LOGO 深棕白描边;倒计时白胶囊、孕周层次;光束摆动加大;水波恢复且避开宝宝 (v26100419)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:28:24 +00:00
a031686ec0 style(首页): 宝宝缩到原图 70%;停用 GIF 只用静图,消除宝宝脸/腿变形 (v26100418)
- 原 GIF 帧间整体扭动(生成动图自带),宝宝脸型、腿型会变 → 9 阶段只用静图
- 静图重做:囊内整块换空孕囊,只贴缩小后的宝宝 + 脐带,原图不残留

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:23:42 +00:00
f3af6bb839 feat(首页): 宝宝来信不再上首页卡,只在底 tab 显示带数字的红圈 (v26100417)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:16:44 +00:00
0ddd6348fb style(首页): 孕囊里的宝宝缩小到 85%,孕囊不变;去掉水波光影 (v26100416)
- 9 个阶段静图 + GIF 重做:抠出宝宝缩小后贴回空孕囊,孕囊外形/大小不动
- GIF 每帧底图用静图,羊水不晃,只有宝宝自己在动;帧差透明压缩,1~3MB → 250~490KB
- hero-fx.js 不再画水波层(光束、微粒保留)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 05:49:48 +00:00
bb53087f7d style(首页): 头图孕囊宝宝整体缩小到 82%,圆心不动 (v26100415)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:34:52 +00:00
72a190fe71 style(首页): 头图与第一块卡片衔接——无来信时工具卡压进头图 (v26100414)
- 无来信:头图 250→262、平底,背景图高度钉在 250,多出部分用底边色补;工具卡 margin-top -26 压上去,两个圆角落在橙色里
- 有来信:头图保持原 250 + 弧底(app.js 同步给 .home-hero 切 .no-letter)
- #homeBody z-index:1 防头图子元素盖卡;彩色光斑下错 26px 不洗白头图
- 工具卡顶部向上淡投影、顶部更实的白、光晕减半;水印下移 10px

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:20:21 +00:00
1637529e3d style(首页): 胎动鼓包更尖;「记一下」叹号每 4 秒跳动一下 (v26100413)
- 手/脚/膝/反踢的 sharp 提高约 0.6~0.8,顶点加集中高光与背光(翻身/屁股不加)
- .fab-w s 加 fabBang 动画:4 秒一轮,前 0.6 秒跳起晃两下;减少动态效果时关闭

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:43:09 +00:00
e4d0c7c5ae revert(胎动): 去掉踢屏幕褶皱与快连踢,胎动恢复到 v26100406 (v26100412)
用户要求撤掉踢屏幕的效果、回到上一版;头图柔光/水波保留。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:22:48 +00:00
2a95a5f3b9 style(胎动): 连踢「噔噔噔噔」每一拍都清楚踢出褶皱 (v26100411)
- 连踢时褶皱回落加快(4.2/s),拍与拍之间退平,不再糊成一片
- 每拍力度加大,褶皱落点略偏
- 4 下快连踢权重最高;26~33 周连踢概率 50%→60%,34 周起 22%→35%

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 03:04:40 +00:00
6c656ecc28 style(首页): 头图柔光与柔和水波、宝宝不再形变;胎动踢出明显褶皱、加快连踢 (v26100410)
- 光束画在 1/6 小画布再放大,宽而柔;水波改为低分辨率正弦大片光影,去掉白色网状焦散
- 去掉孕囊 SVG 位移滤镜;孕囊中心区域水波/光束/微粒避开,宝宝脸不受影响
- 胎动:鼓包外放射状褶皱(左上亮右下暗、三层虚边),每一拍重新踢出、更深
- 连踢加「噔噔噔」快节奏(.22~.26s),26~33 周出现概率 40%→50%

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:48:00 +00:00
3a3ea0a393 style(首页头图): 羊水质感升级——顶部光束、真实水底焦散、液体折射、悬浮微粒 (v26100409)
- 焦散改为游动点细胞边界 + 正弦扭曲的不规则细亮网,两层叠加,囊内更亮
- 光束从顶部斜射,三层软边,摆动明暗;亮度加强
- .hero-baby 挂 SVG 位移滤镜 #mbFluid,羊水折射式轻微晃动
- 新增 34 颗悬浮微粒,经过光束时更亮

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:40:22 +00:00
ca164bc2f8 style(首页头图): 36 周起的宝宝改成头朝上 (v26100408)
用户反馈头朝下看着不舒服;重画足月宝宝(头朝上侧身蜷缩、闭眼闭嘴、撑满羊水囊),重新生成动图与静图。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:20:45 +00:00
e718e8298d feat(首页头图): 宝宝按孕周 9 阶段换形态、孕囊随之缩放;花瓣换成阳光水波 (v26100407)
- 背景换成去掉羊水囊的空背景,羊水囊+宝宝整幅按阶段换(4/8/11/14/18/24/28/32/36 周)
- 每阶段一张本地程序化形变 GIF(呼吸、轻浮、小手、蹬脚;脸不动,闭眼闭嘴)+ 同帧静图打底
- 孕囊直径按阶段 .42~1.03 倍,围绕原圆心缩放
- 去掉花瓣飘落;新增 #heroFx 画布:斜照光束 + 水波焦散,离开首页/不可见时停画
- 逻辑独立在 js/hero-fx.js,调试 ?kidweek=N

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 02:14:41 +00:00
51b1e392aa feat(首页彩蛋): 胎动带节奏、可连续踢 (v26100406)
- 原地连踢:脚/拳/膝按节奏型连顶 3~4 拍(26~33 周 40%,34 周起 22% 且放慢)
- 连续踢:踢完不歇,0.2~0.55 秒后在旁边再踢 1~2 下,每下缩短到 1.1~1.7 秒(30% / 15%)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:07:03 +00:00
966efb17a1 style(首页彩蛋): 胎动只凸起不变色,小脚丫带隐约脚印 (v26100405)
鼓包/摁压的暖红阴影与桃色高光改为中性灰阴影 + 白高光;foot/kick 峰值时叠一层极淡软边脚印(脚跟/前掌/五趾)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 01:01:21 +00:00
3043e37fb5 style(首页彩蛋): 胎动幅度整体加大 (v26100404)
部位尺寸 +20%;元素撑开/放大/上抬/斜切约 1.5~2 倍;卡片鼓起约 2 倍;光影加深;早期小鱼泡泡振幅加大。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:49:19 +00:00
f737d1f18d 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>
2026-10-04 00:36:59 +00:00
cc51237dff style(我的): 卡片改毛玻璃,底下垫彩色光斑 (v26100402)
孕期卡/兜豆横条/心选商城/常用服务/退出登录:半透明白 + backdrop-filter 模糊 + 顶边高光 + 白描边;
page-body 内新增 .me-orbs 四团光斑,页面底色改淡粉→薰衣草渐变;不支持 backdrop-filter 时退回 .82 白。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:20:47 +00:00
5be1c15154 style(我的): 兜豆兑换大卡收成横条,与心选商城同款 (v26100401)
去掉「兜豆兑换」标题和热门 3 件好物,保留 #meExchCard/#meExchSub,点击仍走 openExchange()。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 00:14:51 +00:00
e857826184 feat(首页彩蛋): 宝宝在肚皮底下动——整页随机顶出小帐篷,摁中有震动/声音/压扁滑走/回弹余波反馈 (v26100301)
- 新增 js/bump.js(独立文件,删 index.html 里一行即下线)
- 第一下摁鼓包不触发卡片跳转;宝宝冒头<0.3s内点击照常跳转;不顶小按钮
- 「隐私权限」新增开关「宝宝在肚皮下动」(localStorage mb_bump)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:48:55 +00:00
ec7b6dbe7f demo/bump v4:宝宝活在整页底下,滑到哪个模块就在哪一屏顶出小帐篷
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:08:44 +00:00
ec97586afc demo/bump v3.1:右侧卡片去掉数胎动,改成「这周宝宝学会了」+ 宝宝悄悄话
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:41:57 +00:00
83611d82c5 demo/bump v3:头图重新设计(右侧今日胎动卡+找找宝宝收集条),四种部位顶肚皮,脚趾再弱化
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:28:28 +00:00
cd76d36531 demo/bump v2.1:小脚丫改成踢起软帐篷,字跟着撑起变形,脚丫弱化不吓人
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:11:31 +00:00
6f42ee1499 demo/bump v2:头图改成妈妈肚皮,小脚印时隐时现,戳中就溜,按压肉感回弹
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:59:27 +00:00
c692d25576 创意预览页:宝宝占领首页(demo/occupy)、宝宝把倒计时改了(demo/countdown),头图分层素材 demo/_hero
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:30:11 +00:00
b017cccfae 创意预览页:首页头图被宝宝从里面顶起来(demo/bump,不进正式首页)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:12:21 +00:00
cec4be248e 今日脚印:预览环境追加数胎动/数宫缩/称体重示例行 (v26100233)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:02:25 +00:00
8256fed48b 底部 Tab 下移 6px (v26100232)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:02:05 +00:00
504ea8eabd 首页卡片:加回顶边主题色细线并弱化透明度 .55→.28 (v26100231)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:37:36 +00:00
e4df5dbdcc 首页卡片:去掉顶边主题色细线 (v26100230)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:34:55 +00:00
c248cb8122 首页卡片:轻毛玻璃 + 标题区主题色光晕 + 底层彩色光斑 (v26100229)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:29 +00:00
5608b389be 撤回首页卡片软糖风,回到毛玻璃 v26100228
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:14:47 +00:00
f4932d3d06 首页白卡改软糖风:纯白大圆角+柔投影+角落有机色块+虚化小球 v26100227
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:03:08 +00:00
fd7ce776cc 今日脚印精简版:去日历/小诗/脚丫/卡片/标签/胶带,只留标题日期、最近4条记录、宝宝一句话 v26100226
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:58:58 +00:00
65412829ed 专属福利标题图标换成珊瑚渐变底白色礼盒 v26100225
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:50:05 +00:00
cb399c15a6 福利卡片去掉左上角「孕N周专属」角标 v26100224
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:45:47 +00:00
2ed5ee279f 首页底部盲盒横幅暂时隐藏 v26100223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:36:32 +00:00
c4882a7c56 首页图标「兜」字改细、改小 v26100222
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:27:17 +00:00
d414c32c50 底部首页图标:房子里的爱心换成「兜」字 v26100221
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:20:39 +00:00
ca9e2c1d07 slogan 字间距收紧;消消乐推广改单行矮条(143px→88px)v26100220
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:55:23 +00:00
ebaee6b834 记一下描边回到白色(保留小号背景加号)v26100219
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:51:52 +00:00
2033809ff7 记一下描边中间圈改为圆钮实际显示色 #F86C50 v26100218
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:49:39 +00:00
e2497c40d6 记一下贴纸描边中间圈改为圆钮深橙 v26100217
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:46:24 +00:00
0d1c62c37e 「记一下」描边改成参考图贴纸样式(浅橙粗边+深橙细边),背景加号缩小 (v26100216)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:42:59 +00:00
3db0df5199 「记一下」背后加淡加号,「记」字放大并向左探出 (v26100215)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:34:13 +00:00
3ae3b673e3 底部「记一下」圆钮的加号改成贴纸文字「记一下!」(v26100214)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:26:41 +00:00
931c3215e0 首页白底板块加毛玻璃质感(磨砂颗粒 + 高光 + 亮边)(v26100213)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:56:10 +00:00
7a55dc9423 底部 Tab「我的」头像去掉未选中时的灰圈 (v26100212)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:12:55 +00:00
2eafc08c74 婴儿房恢复滚动视差(撤回 v26100210)(v26100211)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:11:45 +00:00
26a13076fd 婴儿房去掉滚动视差,修复滑动时房子颤动 (v26100210)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:10:33 +00:00
abf5145e36 婴儿房「进去看看」角标改青草绿,和按钮统一 (v26100209)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:23:17 +00:00
84720fcee0 婴儿房「进去布置」按钮改青草绿,和「去兑换」拉开 (v26100208)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:11:58 +00:00
572cb996ed 婴儿房角标「往里看看」改「进去看看」 (v26100207)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:09:36 +00:00
fd538eb6fb 底部导航改暖色毛玻璃,对齐孕期档案 v7 (v26100206)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:00:30 +00:00
f5f597ed1d 婴儿房角标改「往里看看 · 小名的婴儿房」 (v26100205)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:12:15 +00:00
25f7b76057 消消乐 banner 左右边距对齐 16px (v26100204)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:09:06 +00:00
7f20aef305 悄悄话:自己的金星飞出时改金色外发光流星 (v26100203)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:58:09 +00:00
420b297e7e 悄悄话页顶部收紧:标题一行+话题胶囊,说明挪进弹窗 (v26100202)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:48:02 +00:00
ffd0887840 悄悄话本周话题改「对老公说的话」 (v26100201)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:31:41 +00:00
b05faea2a6 今日脚印小诗 它→TA (v26100200)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:25:21 +00:00
8f01f24e24 悄悄话每周话题:首页入口/全屏页/说一句弹窗展示本周话题「对宝宝说的话」 (v26100199)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:21:22 +00:00
0994781f1e 底部导航推荐版:线稿图标/缺口托住记一下/暖白底+淡出/我的用头像/来信新信晃动 (v26100198)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:45:44 +00:00
8f4f77acee 记一下圆钮整体缩小 (v26100197)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:37:57 +00:00
d22c18089c 记一下加号缩小 (v26100196)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:32:11 +00:00
710cd83529 首页兑换标题改「兜豆兑换实物」 (v26100195)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:21:21 +00:00
3aa972fd6a 首页兑换副标题改灰色 (v26100194)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:16:02 +00:00
3021a2821f 首页兑换模块标题改「攒兜豆好物免费兑」,角标用去兑换同款色 (v26100193)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:08:30 +00:00
05db23eba1 记一下圆钮白边变细,改 CSS 绘制 (v26100192)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 13:01:37 +00:00
914f6ea785 记一下圆钮去掉扩散光环 (v26100191)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:54:57 +00:00
26779c207e 孕育工具/今日孕期小知识标题字号对齐今日脚印 18px/900 (v26100190)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:54:28 +00:00
7a22948e29 hero slogan 换站酷小薇字体(10字子集)(v26100189)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:38:12 +00:00
00a2df52cc 宝宝来信 Tab/首页卡跳新版 /laixin/youju/ (v26100188)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:35:51 +00:00
7373930f3f 档案 Tab 跳成长档案 v5 → v7 (v26100187)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:34:52 +00:00
e8f31c65f0 悄悄话页标题改为「每一颗星星,都是一位孕妈的一句话」 (v26100186)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:30:57 +00:00
6b26d5ccbe 此刻:常驻三行同周妈妈使用动态(三种工具错开、可点直达),排在悄悄话入口上方 (v26100185)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:29:31 +00:00
6e55245919 记一下宫格改回一行 4 个 (v26100183)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:00:40 +00:00
d62337953f 记一下弹窗压到半屏以内:标题一行、今天卡合成一条、宫格5列;修关闭弹窗阴影漏到屏幕底部 (v26100182)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:57:43 +00:00
2e2986dc5c 悄悄话入口:星星变小变少、避开文字,右侧改「去看看」 (v26100181)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:21:58 +00:00
0f02c56433 3D 婴儿房模块整块可点,点房子直接进 (v26100180)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:17:14 +00:00
cfb87f27de 首页入口改名:{孕周}周孕妈悄悄话 (v26100179)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:16:18 +00:00
16f3b77841 首页悄悄话入口:条内四角星一闪一闪 + 偶尔两道流星划过 (v26100178)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:14:05 +00:00
6af3d5e80c 悄悄话十三改:每颗星一个固定音符,从左到右五声音阶由低到高 (v26100177)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:03:12 +00:00
bb87127fa8 悄悄话流星十二改:字离白头更远、尾巴字段光变柔,小字不被白光吃掉 (v26100176)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:59:51 +00:00
a68f39040b 悄悄话流星十一改:划走的星消失、点星光波碎星与星尘、合成音效与开关、全部落完收尾 (v26100175)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:58:33 +00:00
534ccc3ca6 悄悄话流星十改:可连点多颗,多颗流星同时滑落 (v26100174)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:55:21 +00:00
911cd0338b 悄悄话流星九改:头部实心发光白、尾巴字两侧白光加亮、字加淡描边保清晰 (v26100173)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:46:56 +00:00
885657fe2b 悄悄话夜空八改:满天四角星有大有小一闪一闪、流星头改白色光滴无十字、尾巴软边 (v26100172)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:42:18 +00:00
3cff549288 悄悄话流星七改:去中线、细体字、首次一闪提示、内容卡流星划完再出且改小 (v26100171)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:39:03 +00:00
7b174623d3 悄悄话流星六改:头窄尾宽光楔、字由小到大、亮头、起步更快后段加速 (v26100170)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:21:32 +00:00
e45306f0dd 悄悄话全屏页五改:流星夜空,点星变流星、字做尾巴先慢后唰、底部内容卡 (v26100169)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:08:57 +00:00
e70e4cb91b 悄悄话全屏页四改:一屏一句竖滑沉浸式、逐字亮起、光晕随句变色、双击抱抱、全部抽屉 (v26100168)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:54:02 +00:00
a9581b64a1 今日待办:勾选框换成墨绿线性图标 (v26100167)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:46:32 +00:00
d75d7b7023 记一下弹窗加厚:日期孕周 + 今天进度卡 + 先记这件 + 宫格已用打勾 (v26100166)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:39:35 +00:00
acc8301b20 同孕周悄悄话全屏页三改:星图 + 聚焦卡 + 清单;运动助手待办文案 (v26100165)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:14:34 +00:00
c4c4d8ceac 好物价格改为金币在前 + 千分位 + 淡色兜豆,对齐商城本场好物 (v26100164)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:52:05 +00:00
fb116ddb5e 兜豆兑好物拆掉内层粉卡,和其它模块一样铺平 (v26100163)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:14:41 +00:00
9095ce6600 首页兜豆兑好物图标换成统一豆形金币 dou-coin.svg (v26100162)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:10:46 +00:00
5df6d7638c 标题去红统一墨色 + 同孕周悄悄话改名 + 来信卡再短 (v26100161)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:51:19 +00:00
22b2d1e058 盲盒挪到首页底部低调细条 + 来信卡居中短版 (v26100160)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:49:02 +00:00
e065a47ccb 此刻收拢 + 同周悄悄话改为细入口 + 全屏「深夜星河」页 (v26100159)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:27:55 +00:00
8b749633d6 style(首页): 来信卡四版 + 盲盒横幅素雅版,去掉艳色渐变 (v26100158)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:18:04 +00:00
a748d2ed03 docs(AGENTS): 登记悄悄话模拟句例外与后端未入库风险
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:04:58 +00:00
9279fd921b feat(此刻): 陪伴版文案 + 7 个头像 + 同周悄悄话(预设句/自由输入、抱抱) (v26100157)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:04:39 +00:00
d72b40f0d7 孕周专属福利卡去掉右上角「免费送」印章
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:37:39 +00:00
44f0cfeec1 宝宝来信卡三版:左侧玫瑰色封口块 + 探出卡沿的矢量信封 + 未读数 + 圆形箭头
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:26:16 +00:00
fcac03ea59 今日脚印:无记录时改为「今日待办」清单;宝宝今天想说去掉 AI 角标
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:23:09 +00:00
39c27e4bf2 首页去胶囊雷同:此刻/孕周/本周第N天/今日小知识/日圆点改纯文字层级,只保留行动按钮为实心
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:03:09 +00:00
18feb14b4b 宝宝来信卡二版:去掉斜纹边/邮戳/横线,改为白色磨砂卡 + 粉色圆底信封
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:13:37 +00:00
5dc647af3c 首页宝宝来信卡改版:航空信封斜纹边、爱心邮戳、未读数气泡、手写标题、胶囊按钮
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:01:11 +00:00
53bb0f6bcb 婴儿房封面换新图(屋后水杉林)v=20261001p
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:14:58 +00:00
32ddb04b71 slogan 去掉「做」,改用系统黑体
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:56:17 +00:00
add92309b2 消消乐角标改为「兜爸旗下小游戏」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:51:56 +00:00
7ae14d6df9 盲盒横幅提亮:莓红→紫罗兰渐变 + 白字 + 金色按钮
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:51:31 +00:00
ca2d69f6a5 slogan 改为「做全家都爱用的孕育平台」;消消乐角标改「兜爸心选旗下小游戏」;兜豆兑好物标题放大
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:49:44 +00:00
5601f44e29 首页底部三模块拉开色相:脚印薄荷绿、消消乐糖果紫、福利蜂蜜金
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:45:15 +00:00
7934b91865 档案 Tab 改跳孕期档案 H5(chengzhang-dangan/v5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:36:14 +00:00
9a1ff38d94 底部导航选中项去掉背景胶囊,改为颜色+小圆点
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:34:54 +00:00
a688ce3a81 首页兜豆兑好物套上与其它模块一致的白色外壳
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:31:35 +00:00
5da526fc5c 底部导航改版(选中胶囊+升起的记一下圆钮);今日脚印无记录时显示两条示例
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:29:56 +00:00
49cc7973d2 本周孕育指南改为孕周专属福利(资料免费送福利券样式)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:26:53 +00:00
3f115bd37f 今日脚印改版(撕页日历+小脚丫时间线+宝宝便签),消消乐卡压矮
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:22:55 +00:00
da995ae540 首页兜豆模块加横滑可兑好物;我的页孕期卡与兜豆卡对调;资料弹窗改版(实时结果条、预产期三种填法、孕周步进、小名建议)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:12:25 +00:00
96b04877de 兜豆兑换入口接入:跳 doudou-duihuan(带 openid/return_url)、余额与可兑件数、本场热门好物;配色改用商城 berry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:00:10 +00:00
f1ab738865 我的页改版(蜜桃珊瑚头部 + 兜豆兑换商城主卡 + 服务宫格);首页新增兜豆兑换商城入口
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:56:51 +00:00
ac72585ecb 首页头部左侧减负:去掉与孕周重复的「已经生活了 N 天」玻璃卡,剩倒计时/大孕周/水果进度条三层,间距拉开 (v26100135)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:44:25 +00:00
782f0b8c58 婴儿房上下沿改为渐隐融进页面底色(去掉两道硬阴影),夜间渐隐收短防发灰 (v26100134)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:03:56 +00:00
8ca26162ce 婴儿房封面换 3D 婴儿房 v=20261001h(写实树木+远山田野满幅远景):图铺满缝宽、去羽化、下移 30px 露出远山;图片加 ?v= 缓存参数 (v26100133)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:37:37 +00:00
df7c071d3c 孕期小知识详情页加「本周每日小知识」(今天在上,倒排到第 1 天,后几天锁着);首页今日小知识大卡也可点进详情页,圆点仍为切换 (v26100132)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:18:01 +00:00
b3b768722a 孕期小知识:首页四张卡去掉文字,改成图标+标题四个入口;点进详情页 #detailTips 看四张卡全文(滚到对应卡并高亮)(v26100131)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:09:56 +00:00
19e0167df0 孕期小知识四张小卡精简:收起只露一句重点(宝宝=大小比喻,其余取首个分句),点开看全文,标题加小箭头 (v26100130)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:05:33 +00:00
eb1c1b51f1 婴儿房封面换写实版(3D 婴儿房 cover ?v=20261001g,1125 原尺寸 JPG);底色 白天 #DCE5EC / 夜间 #2C3350,图左右下沿羽化防草地接缝,夜间玻璃条改深蓝 (v26100129)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:31:47 +00:00
f0428fea99 首页孕期小知识与 3D 婴儿房对调位置:此刻 → 孕期小知识 → 婴儿房 → 今天的动线 (v26100128)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:39:10 +00:00
0e0c23c806 孕期小知识恢复上一版:首页完整卡片(今日小知识+7天圆点+四张横滑卡+免责),撤掉详情页 #detailTips (v26100127)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:29:39 +00:00
6bf7fa4058 首页消消乐与 3D 婴儿房对调位置
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:24:04 +00:00
ed05acdf55 首页头部左侧改版:见面倒计时胶囊 + 大孕周 + 水果进度条 + 陪伴天数卡,替代孕周圆环
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:21:13 +00:00
9ca25d05de 婴儿房封面换整栋小别墅:新图、缝高 340、图宽 88% 上提 40px,标题改「我们的家」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:11:24 +00:00
e7624ca484 工具区大字水印「妈妈」改成「兜爸」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:03:20 +00:00
906da99a6e 首页 LOGO 缩小 40→30px
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:50:29 +00:00
b05862496d 首页左上角「兜爸心选」文字换成正式 LOGO(白色透明版,适配橙红头图)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:46:51 +00:00
ff633c3ad5 孕期小知识:首页只留标题+一行摘要+四个入口,全文挪到详情页 #detailTips
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:54:33 +00:00
ae16994bcc 工具卡右下角水印 logo 淡一点小一点(28%→18%,尺寸约 8 成)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:48:31 +00:00
e6ac933393 头图宝宝换母婴作图 v8 动图(摇摆+蹬脚,4 秒循环)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:30:40 +00:00
b83faa77c2 style(首页): 此刻减到三行(她们在做合成一句、删今天别忘了、动态压成一行小字);婴儿房空隙恢复 v26100114 版式(用户澄清:要改的是此刻) (v26100117)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:10:01 +00:00
fcabec28dd style(首页): 婴儿房空隙减字放大——去掉小标签和毛玻璃横条,只留大标题+一句短话+居中大按钮,空隙加高到 380 (v26100115)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:55:32 +00:00
3eea304818 fix(首页): 婴儿房空隙图改用 1125×1500 原尺寸(3 倍屏不再发糊)(v26100114)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:46:53 +00:00
3de3bbdccc feat(首页): 婴儿房空隙换成婴儿房导出的无 UI 高清渲染(日/夜两版,夜间底色 #5A5890)(v26100113)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:43:59 +00:00
a90f348061 feat(首页): 3D 婴儿房入口三版——今天的动线与书架之间开一道空隙,房间铺在整页背后随滚动视差露出,「进去布置」按钮跳入并带 return_url (v26100112)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 08:40:05 +00:00
3394a1f6b0 feat(首页): 3D 婴儿房入口改版——首页最底的「走廊尽头的门」:门缝里房间随孕周布置、木牌+入住倒计时日历、宝宝气泡轮播、点击推门进入 (v26100111)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 07:57:59 +00:00
df95d1fae0 feat(首页): 新增 3D 婴儿房入口卡(小房子造型、昼夜两版),放在今天的动线与书架之间 (v26100110)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 07:33:37 +00:00
4637412e26 feat(此刻): 收进孕育工具卡底部做轻量条,删「她们在聊」和全站今日三项 (v26100109)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 04:19:37 +00:00
d270f09bf7 feat(此刻): A+B 改版——和你一样孕 N 周的妈妈们(在做/和你比/今天别忘了/在聊/同周动态),去掉应用 logo 改用妈妈头像 (v26100108)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:24:15 +00:00
555012f248 feat(盲盒): 价值感改造一期前端 —— 待解锁任务 / 宝宝周记 / 集齐奖励 / 下周预告 (v26100107)
- 待解锁:错过的周 + 三个工具任务(进度、去完成跳应用、解锁按钮),卡册格子 🔒
- 翻卡层加「宝宝周记」;卡册点开带周记与宝宝的话
- 卡册顶部集齐奖励:三段 + 全集,差 N 张 / 待发放 / 已发放 / 填收货信息
- 集齐庆祝弹层 + 收货信息表单(POST /blindbox/claim)
- 本周已开时显示下周卡片剪影 + 倒计时;横幅优先提示可解锁 / 离 XX 大奖还差 N 张

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 03:15:16 +00:00
ec87d36db2 fix(首页): 按下「孕期体重」等工具卡时卡片被撑长 (v26100106)
.bento .mini-card>*:not(.mc-art) 把点击时插入的涟漪 .rp 也改成 position:relative,
涟漪(卡宽 2.1 倍的圆)变成占位元素,flex 列布局的体重卡从 232px 撑到 ~590px。排除 .rp。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:47:28 +00:00
1fa565ff2f fix(首页): 禁止双指缩放和双击放大,页面固定 (v26100105)
viewport 加 user-scalable=no / maximum-scale=1;iOS 微信忽略该设置,
JS 再拦 gesture 事件与多指 touchmove;html 也加 touch-action:manipulation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:40:34 +00:00
15dabcea5f style(首页): 孕期小知识字号放大,正文 14px、小卡 13px (v26100104)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:36:23 +00:00
9013a84dea fix(首页): 删除妈妈的树洞 / 预览页不再自动跳上次应用 / 「我的」加盲盒卡册常驻入口 (v26100103)
- 妈妈的树洞整块删除(HTML/CSS/JS),需要恢复看 git 历史
- 预览页 /mama-box-preview/ 跳过冷启动恢复上次应用,避免一打开链接就跳到孕检日历
- 「我的」设置与服务里加「我的孕期卡册 N/37」,与首页横幅同一入口;未建档/未到孕周时隐藏

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:21:09 +00:00
70c9786f15 feat(首页): 此刻卡换应用 logo / 树洞星夜版 / 孕期小知识带孕周并压缩 (v26100102)
- 此刻:今日三项与动态的图标换成对应应用的 logo(取 App 表 logo,后台换即生效);
  消消乐、盲盒用本站同风格 logo(assets/img/429ffcba0d6e.svg、b0087c3c012a.svg)
- 妈妈的树洞重做为星夜版:深夜天幕 + 剪影树 + 旋转发光的漩涡树洞;每条心事化成天上一颗星(位置按时间戳固定);
  扔进去时字逐个被吸进漩涡、洞口爆亮、新星射上天空;整块比旧版更矮。心事仍只存本机,热线兜底不变
- 孕期小知识:标题改「孕 N 周 · 今日孕期小知识」,右侧「本周第 N 天」;今日卡与圆点收紧,四张小卡改一行横滑(点开看全文)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 02:10:13 +00:00
c414a7ff8f feat(首页): 孕期盲盒横幅 + 盲盒页(开盒动画/翻卡/卡册/概率公示)+「此刻,孕妈们在做什么」卡 (v26100101)
- 横幅:顶部大图下方,按 /blindbox 状态切换文案(可开 / 补开 / 下周倒计时 / 填预产期引导),礼盒晃动 + 扫光
- 盲盒页:开盒舞台(光芒旋转、礼盒悬浮,开启时越晃越烈→盖子飞起→白光+彩纸→3D 翻卡),闪卡镭射光;
  卡册按孕早/中/晚期分组,补发标「补」,错过的周可补开;规则与概率说明由接口下发
- 此刻卡:大数字进入视口时数上去、每分钟平滑刷新;今日三项可点进工具;匿名动态每 3 秒上翻
- 新增 14 张周卡插画(其余孕周先用本周水果占位)
- AGENTS.md §4.12 登记模拟数据例外(决策人/日期/期限 2026-12-31)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:44:44 +00:00
4c550882fe fix(预览): 示例数据文案缩短,避免孕检日历、体重卡折行 (v26093025)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:13:34 +00:00
da740ba07e feat(预览): 预览页五张工具卡填一组示例数据 (v26093024)
只在 /mama-box-preview/ 路径下生效,用于看排版效果;正式首页 /mama-box/ 不受影响,
照旧显示真实数据或「--」(AGENTS §4.2 不造假数据)。示例数据按 tool-cards 接口结构
直接喂给 applyRemoteCards:数胎动 12 次、体重 58.2kg、糖耐 10月8日、估重约 1250g、宫缩间隔 5分20秒。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:13:01 +00:00
555befc08b style(首页工具卡): 水印图标整体缩小约 20% (v26093023)
透明度、角度、右下角出血的摆法不变,只按比例缩小尺寸和出血量。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:09:35 +00:00
53b208e3a5 style(首页工具卡): 图标改成白色半透明水印,降低视觉干扰 (v26093022)
用户反馈五个彩色图标抢眼、满屏都是图标。图标只作装饰,统一改为:
- 白色单色 SVG(彩色细节改用镂空),28% 透明度;
- 放大并转 -12°,斜压在卡片右下角、出血到卡边外,退成卡面底纹;
- 孕期体重卡的秤也改为右下角水印,标题和数字居中;文字层统一压在水印之上。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:06:48 +00:00
a7e71fda2d feat(首页): 删掉孕期每日小打卡;工具卡图标只留 LOGO 里的图形并改矢量 (v26093021)
- 删除首页「孕期每日小打卡」模块:HTML、.daily-check/.dc-* 样式、renderDaily 及产后收起逻辑、
  三张专用图标。工具区重新成为首页第一块(首块间距规则本就按 :first-child 走)。
- 五张工具卡图标去掉 LOGO 外面的圆角方块 / 圆环底,只留里面的图形,全部改成 SVG:
  数胎动小脚丫、胎儿估重表盘、数宫缩波形直接取自工具页原 SVG;
  孕期体重(原图 128px webp)、孕检日历(原图 100px png)原图太小放大发糊,照原图配色重画为矢量。
- 图标位置微调,避开「记录」按钮和标题。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:00:21 +00:00
d164688743 feat(首页工具卡): 图标换成各工具页左上角的 LOGO (v26093020)
- 五张卡的扁平图标换成点进去那个工具页左上角的 LOGO 原图(透明 PNG):
  数胎动 .brand-logo、孕期体重 img.brand-logo、孕检日历 .hero-brand i、胎儿估重 .lg、数宫缩 svg.brand-logo。
- 孕检日历原图四角有锯齿缺口,补成连续圆角。
- LOGO 不再出血到卡边外,统一离右下角约 10px、挂同一档投影;尺寸按卡片空间重新摆放,
  避开「记录」按钮和标题。卡片底色、版式、跳转、数据回填不变。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:50:21 +00:00
37bdfa6f24 feat(首页工具卡): 图标改扁平化,卡底色跟随各工具页色系 (v26093019)
- 五个图标(小脚丫 / 体重秤 / 台历 / 秤盘小豆宝宝 / 秒表)重画为扁平 SVG:白色 + 同色系实色块,
  去掉渐变、阴影、高光和 3D 大眼睛;去掉插画的 drop-shadow。
- 卡底色改成点进去的工具页同色系、稍深一档:数胎动陶土橙、孕期体重珊瑚粉、孕检日历草绿、
  胎儿估重雾蓝、数宫缩薰衣草紫(取自各页 theme-color / 主色,见 CSS 注释)。
- 「记录」按钮字色跟数胎动卡改成陶土橙。版式、跳转、数据回填不变。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:33:42 +00:00
4a7eb92864 feat(首页工具卡): 五张卡按参考图改版——糖果色底 + 白字 + 3D 大眼萌物插画 (v26093018)
- 数胎动 / 孕期体重 / 孕检日历 / 胎儿估重 / 数宫缩:饱和渐变底(蓝 / 粉 / 绿 / 紫 / 蓝紫)+ 白色粗标题,
  插画换成自画 SVG 萌物(小脚丫 / 体重秤 / 台历 / 秤盘小豆宝宝 / 秒表),立体光泽、黑色大眼珠带高光。
- 只换皮:高度账、排布、data-mini 跳转、各 id 数据回填不变;「更多工具」出口保持灰米。
- 「记录」按钮改白底蓝字并压在插画上;告警态在彩色底上改亮黄字(原 --alert 红看不清)。
- 数宫缩卡原曲线插画 72f41378ef8e.png 已无引用,删除;其余旧 PNG 仍被「每日小打卡」使用,保留。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:23:36 +00:00
18eecdb3a2 docs(css): 头图注释里的背景文件名同步到 v7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:18:29 +00:00
c73a1419ec feat(首页头图): 孕25周换母婴作图 v7 GIF——改回侧身闭眼睡、正脸可见 (v26093017)
- 用户否了 v6 的背对造型,素材换 v7:回到 v5 姿势(侧身、闭眼睡、小手在嘴边),画风稍卡通、脐带更清晰。
- 构图和囊位置同 v6,接入方式和摆位数全部沿用(静帧 .88 铺底 + GIF 截 450×450 整高块叠加)。
  v7 宝宝动作实测离截块中心最远 ≈168px < 遮罩实心半径 180px;首帧和背景对齐误差 ≈3.5(GIF 抖动噪点级)。
- 底色兜底 #dc8569(=新图左缘色)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:16:10 +00:00
8c25d5fc74 feat(首页头图): 孕25周换母婴作图 v6 GIF——宝宝背对镜头、脐带更明显 (v26093016)
- 素材换母婴作图 v6(替代 v5 的 mp4):卡通动漫风宝宝背对镜头不露脸,脐带从囊壁右上方垂下、全程不动;
  画幅从 1280×736 变 16:9。
- 背景 = v6 静帧缩 .88、截取块中心 (656,360) 摆到 (1240,375),囊完整不裁;
  <video> 换成 <img>:原 GIF 截 450×450 整高块(含宝宝全部动作),object-fit:cover 铺满 .hero-baby,
  首帧和背景对齐误差 ≈3.7(GIF 抖动噪点级)。加载完淡入,没加载出来停在静帧。
- 宝宝不露脸了,嘴边的小气泡连同样式一起去掉;底色兜底 #df856a。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 16:02:41 +00:00
488989d82c feat(首页头图): 孕25周换母婴作图 v5——不规则羊水囊、去胎盘、囊壁细脐带 (v26093015)
- 素材换母婴作图 v5(替代 v4):羊水囊改不规则有机形状,去掉囊顶胎盘,
  脐带从囊壁右上方细细淡淡垂向宝宝身前、全程不动;宝宝吸手指时头部 ±15px 低头抬头。
- v5 的囊比 v4 的圆宽,背景缩放 .93→.90、囊心摆到 (1240,375),囊右缘不再被切;
  叠加视频以囊心 (661,381) 截 660×660→400×400,54KB;.hero-baby 摆位按新几何重算,
  首帧和背景对齐误差 ≈2.4(压缩噪点级)。嘴边气泡起点跟着挪到新嘴位。底色兜底 #de8868。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:47:20 +00:00
05ce51300e feat(首页): 新增「宝宝消消乐」游戏推广入口 (v26093014)
- 工具托盘正下方一张糖果色大卡:「新游戏」角标 + 名字 + 玩法一句话 + 跳动的「开始玩」,
  右侧 3×3 待产用品小棋盘循环演示三连消除,角上星星闪烁。
- 点击走 jumpApp() 跳 https://beian3.douba.top/myxx/index.html(游戏由「母婴消消」维护,
  不需要身份参数,多带无害);GAME_URL 为空时整块隐藏。
- 动效在 prefers-reduced-motion 下关闭;页面隐藏时棋盘动画暂停。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:29:07 +00:00
9a1ad1fb93 feat(首页): 新增「孕期小知识」和「妈妈的树洞」两个模块 (v26093013)
孕期小知识(工具区下方):
- 按真实孕周 + 天数显示「今日小知识」大卡,右上角本周水果 emoji 缓慢浮动;
  底部 7 个圆点 = 本周第 1–7 天,今天高亮呼吸、以前可回看、以后锁着(明天见)。
- 四张马卡龙小卡:宝宝这周(带大小比喻)/ 妈妈身体 / 吃点什么 / 这周留心,进入视口依次浮上来。
- 内容 assets/data/preg-tips.json:孕 4–40 周,每周 4 句 + 每天 1 条,共 259 条,按需 fetch。
- 只在 /pregnancy 确认真实孕周后显示(pregEmpty === false 且非产后),不拿默认孕周配内容。

妈妈的树洞(今日脚印下方):
- 心事只存本机 localStorage(mmbox.treehole.v1,最多 200 条),不上传;按用户选定不做回应,
  只有纸团飞进树洞 + 树冠晃动 + 「已收好」动画;可展开「我的心事」回看、删除。
- 出现明显轻生字眼时,在「已收好」下加一行 12356 心理援助热线(兜底);卡底常驻一行求助提示。

动效在 prefers-reduced-motion 下关闭。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:23:00 +00:00
9ea9f3c50f feat(首页): 新增「孕期每日小打卡」——按钮带状态小标签 (v26093012)
- 首页正文最上面加一块「孕期每日小打卡」:数胎动 / 称体重 / 产检三行,
  每行右侧一枚状态标签,点整行进对应应用(复用 data-mini + MINI_APP_MAP + jumpApp)。
- 标签来自工具卡同一个接口(applyRemoteCards → renderDaily),不另打请求:
  今日已记 N 次 / 今天还没记;今日已称 / 今天还没称(按接口 day 判今天);
  距 NT 还有 7 天 / 今天糖耐检查 / 待补记 / 看产检安排。
  抬头右端「今日已完成 x/2」(只数胎动、体重两件每日事)。
- 接口没回、访客态保留中性提示,不写数字;产后态整块收起。
- 首块间距规则改为 .home-body>:first-child,工具托盘并入块间 16px。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:10:04 +00:00
62b23af323 feat(首页头图): 孕25周换 v4 用户确认版——胎盘在囊顶、胎发、颜色回调 (v26093011)
- 素材换母婴作图 v4(用户已在作图会话确认画面):颜色比 v3 深一档(囊外 #DB8263~#E38E6E),
  胎盘移到囊顶、脐带从上方细细垂下、接肚子端被腿挡住;头部重画带细软胎发;浮动 ≤7px。
- 背景、囊视频按同一流程重做(囊心 667,368),视频 66KB;底色兜底改 #de8968。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:04:51 +00:00
bca92312f8 feat(首页头图): 孕25周宝宝换 Q 版卡通、整体提亮 (v26093010)
- 素材换母婴作图 v3:Q 版卡通宝宝(圆脸、闭眼笑、粉嫩,无写实纹理),浅暖桃底;
  脐带/胎盘固定、无孕肚、镜头固定、正倒放首尾同帧等 v2 要求保留,整体晃动已防抖。
- 背景、囊视频按同一流程重做(囊心 664,368),视频 74KB。
- 背景变浅,左侧压字暖色层 .14→.20,底色兜底改 #eaa485。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:33:19 +00:00
983191a752 feat(首页头图): 换母婴作图 v2 孕25周视频——脐带连囊壁胎盘、镜头固定、暖底 (v26093009)
- 素材 yun25w-loop.mp4 v2:胎盘在囊内壁左下,脐带全程固定,接肚子那头被腿挡住;
  无孕肚;镜头固定;本身正放+倒放首尾同帧,直接 loop。
- 背景 = 视频第 1 帧缩 .93、囊心摆 (1250,375),左/上/下按边缘色补齐并宽羽化(455d2517fafd.jpg)。
- 视频只截羊水囊 660×660 → 400×400(f20175ae1170.mp4,95KB),宽羽化遮罩叠在背景同位置;
  与背景同源同帧,边缘无缝。去掉单独的静态囊图层。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:10:45 +00:00
d5ee5c8654 fix(首页头图): 羊水囊边缘和外部背景柔和过渡,去掉割裂感 (v26093008)
- 静态囊图边缘改为 r232→300 平滑羽化,底图在囊外加一圈很柔的暖光,把囊壁化进底色。
- 去掉 .hero-sac 的 CSS 椭圆遮罩(横竖半径不同,把囊壁左右削掉、上下留着,是割裂的主因)。
- 视频遮罩放宽为 74%→96%,和静态囊壁柔和叠合,镜头推近时也不出边。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:30:17 +00:00
9bb3c5a533 feat(首页头图): 背景改暖桃橙,和羊水同色调顺下来 (v26093007)
- 头图底子从暗紫玫瑰改成取自囊内羊水的暖桃橙渐变,光晕和散景光点同步改暖、调淡;
  左侧天数卡那块也是同一色调。压字渐变改成很淡的暖色(.14)。
- 静态囊图按新底子重新合成(视频不变)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:23:51 +00:00
f21af1e25b feat(首页头图): 羊水囊不再动,改成囊里的宝宝自己动(吸手指、蹬腿) (v26093006)
- 去掉整层浮动和整图「胎动」扭动(用户:球不要动,要里面的宝宝动)。
- 母婴作图的 25w.mp4 取前 10s,只截囊内 600×600,正放 + 倒放拼成 20s 无缝循环,
  400×400 / 288KB(assets/video/b6476cfce328.mp4),遮罩只露囊内,叠在静态囊图上。
- 静态囊图换成视频第 1 帧(70812d56ec5f.jpg),视频淡入时画面对得上;放不了就停在静态图。
- 微信 iOS 等 WeixinJSBridgeReady 再 play();页面隐藏时暂停;减少动态效果下不放。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:20:13 +00:00
b69a02e570 chore(首页): 先关掉顶部「未获取到微信身份」黄色提示条 (v26093005)
产品要求先不展示。只在 showIdNotice() 入口加开关 ID_NOTICE_ENABLED=false,
判定逻辑、标记、重试按钮都保留;没身份时改为 console.warn,排查不受影响。
恢复展示:把开关改回 true。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:16:30 +00:00
d2c04194fa feat(首页头图): 宝宝单独成层,加「胎动」——时不时扭一下、蹬一下 (v26093004)
- 头图拆两层:散景光点底子 7dfe5be4e828.jpg(背景,不动)+ 羊水囊宝宝 bc6367fcad38.jpg
  (从同一张合成图原样切出,按橙区高 H 换算摆回原位,径向遮罩羽化边缘)。
- 外壳慢浮 6s;图片「胎动」7.3s 一轮:大半时间静止,突然小幅转动 + 位移,随后再轻蹬一下。
  两个周期错开,不像固定节拍。嘴边气泡挂在同一层,跟着宝宝一起动。
- prefers-reduced-motion 下全部停住。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:14:27 +00:00
a0119760ed feat(首页头图): 去掉孕肚、宝宝缩小、左侧提亮,四周改散景光点 (v26093003)
- 孕肚弧线太写实(用户反馈有巨物感),只从原图抠出羊水囊 + 宝宝,缩到约 8 成放在右侧。
- 底子按原图色调重铺紫玫瑰渐变,比上一版亮;囊后一圈柔和暖光晕保留包裹感,周围散景光点。
- 左侧压字渐变从 .5 降到 .22,明显变浅。
- 嘴边气泡锚点按新图重新定位(右 0.408H、顶 0.491H)。
- 新图 assets/img/ab3c7e20811a.jpg(1560×750,59KB),替换上一版预览图 4cd9891cec31.jpg。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:01:18 +00:00
44e20cb0c3 feat(首页头图): 孕25周整图铺成头图背景,左侧顺原图色调延伸 (v26093002)
- 橙色渐变底 + 右上角圆形宝宝图 → 整张孕25周图(孕肚弧线 + 羊水囊宝宝)作 .home-hero 背景,
  右对齐、高度=头图高;左侧按原图左缘色调横向补出暗粉紫,屏宽 ≤520 都铺得满。
  叠一层左深右透渐变压住孕肚亮边,保证左侧白字可读。头图高度 250 与底部椭圆弧不变。
- 去掉右上角圆形图(上一版预览的 dd05df88ab23.png)和三团橙色光斑;
  嘴边气泡改挂在零尺寸锚点上,按背景图里嘴的位置定位,不再整体浮动。
- 新图 assets/img/4cd9891cec31.jpg(1560×750,84KB)。旧正式图 b00c707f7b1f.png 保留。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 07:45:05 +00:00
12fcc037dc feat(首页头图): 顶部宝宝主图换成孕25周图 (v26093001)
原图 25w-original.png(1536x864 横图,来自母婴作图)以羊水囊为中心裁 760x760 正方形,
套圆形透明遮罩后缩到 379x379,与旧图同尺寸(显示宽 189.5px 的 2 倍图),头图布局账不变。
文件名按规则取内容 md5 前 12 位;旧图 b00c707f7b1f.png 保留,避免缓存着旧 HTML 的用户图裂。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:48:41 +00:00
88 changed files with 8687 additions and 371 deletions

View File

@@ -7,8 +7,9 @@
## 1. 这是什么
微信小程序里的**首页 H5**:小程序首页壳(`crmeb/pages/index/index.vue`)用 web-view 硬编码加载
`https://mp.yizhezhe.com/mama-box/`,用户看到的就是本站。它同时是 **H5 应用宫格**入口(30+ 第三方应用的门面)。
微信小程序里的**首页 H5**:小程序首页壳(`crmeb/pages/index/index.vue`)用 web-view 加载本站。
⚠️ 2026-10-09 起首页地址**由 box_app 后台动态配置**(`GET /api/mp/public/home/config` → `result.url`),
不再硬编码;当前指向 `https://mp.yizhezhe.com/mama-box-preview/`(用户确认),默认/兜底仍是 `/mama-box/`。它同时是 **H5 应用宫格**入口(30+ 第三方应用的门面)。
- 纯静态站点:**无框架、无构建、无测试**,`index.html` 一个页面用 `.screen` 切换多个屏。
- 仓库 `jone/mama-box`;本地 `/home/jone/workspace/mama-box`;生产 `/opt/app/mama-box`(nginx alias 托管,非 box_app 挂载)。
@@ -80,6 +81,43 @@ nginx:`root /opt/app` + `Cache-Control: no-cache` + `etag on` —— 缓存靠
⚠️ 卡是固定高度 bento,只有 kick / weight 有位置放第二行(`.mc-extra`);给 fweight/cal/curve 加第二行前
先重排那笔高度账(`158+10+64=232`,见 `mama-box.css`)。
12. **⚠️ 第 2 条「不造假数据」的唯一登记例外:首页「此刻,孕妈们在做什么」卡(2026-10-01 起)**
- 内容:该卡的总人数 / 今日三项 / 匿名动态在**模拟阶段**为 box_app 按规则生成的模拟数字(`GET /pulse`,确定性哈希、不落库、不进任何报表)。
- 决策人:产品负责人(用户本人),2026-10-01 拍板「前期先用模拟数据,要显示人多;正式页先用模拟,真实数据跟上后替换」;已书面告知虚假宣传风险(《反不正当竞争法》第 8 条)。
- 期限:**最晚 2026-12-31** 复核(≤ 3 个月);真实数据接通后在后台把 `MamaBoxConfig.pulseMode` 改成 `real`(或 `off`),无需发版。
- 2026-10-02 扩展:卡内新增「同周悄悄话」(`GET/POST /whisper`、`POST /whisper/hug`)。mock 模式下真实悄悄话与 box_app 生成的**模拟悄悄话**(id 以 `m_` 开头,确定性哈希、不落库)混排,跟随同一个 `pulseMode` 开关;real 只出真实的。⚠️ 模拟句是「假装有人说过的话」,比模拟数字更敏感,切 real 时务必一并去掉。
- ⚠️ 悄悄话后端(`models/mamaBoxWhisper.py`、`services/mama_box_whisper_serv.py`、`mama_box_router.py` 与 `models/__init__.py` 的改动)是 2026-10-02 **直接改在服务器 `/opt/app/box_app` 上的,没进 git**(当时的账号无 box_app 推送权限;改前备份在 `/opt/app/box_app_bak_202610021000`)。服务器下次 `git reset --hard` 前,必须由有权限的人先把这几个文件提交进仓库,否则接口会丢。
- 边界:**只限这张卡**。孕期盲盒的数字(已收集、本周开盒人数)一律真实;任何别处都不得引用这里的模拟数字,前端也不得在接口失败时自造数字兜底。
13. **跳其它 H5 应用 / 调第三方接口一律同源:只写相对路径 `/<slug>/`,不写域名**(2026-10-09 用户定)
- 背景:小程序 web-view 只放行业务域名 `mp.yizhezhe.com`,直连 `taidong.douba.top`(或任何 `*.douba.top`)会被拦;
首页直接 `fetch` 别的域名还会跨域。所有第三方 H5 都由 mp 前置机(8.145.39.61)nginx 反代过去,路径不变。
- 写法:`const XXX_URL = '/<slug>/';`、接口 `'/<slug>/api/...'`。确实需要完整地址的地方(如 `return_url`、上报 last_app)
用 `location.origin` 或 `new URL(path, location.origin)` 拼,**也不要手写域名**;万不得已写死时只能是 `https://mp.yizhezhe.com/...`。
- **新接一个应用前,先 `curl -s -o /dev/null -w '%{http_code}' https://mp.yizhezhe.com/<slug>/` 测:**
- 200 → 直接用相对路径;
- **404 → 说明 mp 还没反代这个目录**(不是「小程序白名单没加」,别等别人)。去 mp 的 `/etc/nginx/conf.d/default.conf`
把 slug 加进「第三方 H5 应用统一反代」那条 `location ~ ^/(...)/`(页面和 `/api` 都在目录下时一条就够),
再加一行 `location = /<slug> { return 301 /<slug>/; }`。改前 `cp -a` 备份,`nginx -t` 通过才 `nginx -s reload`,
之后对比 `mp` 和 `taidong` 两边同一路径的内容应一致,并复查 `/mama-box/`、`/mama-box-preview/`、`/api/mp/...` 仍 200。
⚠️ 这台机同时跑正式站,改 nginx 前先告诉用户。
- 应用的页面或接口不在 `/<slug>/` 目录下(比如接口在根路径 `/api/xxx`)→ 别在 mp 上加兜底,先找该应用的会话改成目录内。
- 历史:2026-10-09 一次补了 `chengzhang-dangan`、`muying-xiaoxiao`、`yinger-fang`、`doudou-duihuan` 四个(备份 `default.conf.bak.4proxy-20261009222036`),
首页这 4 个入口同日改为相对路径(v26100906)。例外:微信 JS SDK `res.wx.qq.com` 是官方脚本,不在此列。
14. **任何 H5 应用「返回首页」一律调 `wxAuth.goHome()`,禁止写死首页地址**(2026-10-09 用户定)
- 原因:首页地址是后台动态配置的(见第 1 节),写死 `https://mp.yizhezhe.com/mama-box/` 会把用户送回旧首页。
`goHome()`(`js/wx-auth.js` ⑦)先拉 `/api/mp/public/home/config` 再跳,自动续接 openid/unionid;1.5s 超时或接口坏 → 回退 `/mama-box/`。
- 写法:`if (window.wxAuth && typeof wxAuth.goHome === 'function') { wxAuth.goHome(); return; }`,
原来拼身份的写死地址只允许留作 **wxAuth 没加载时的兜底**。
⚠️ `goHome()` 的身份只认 URL + sessionStorage(`identity()`)。应用若把身份从 URL 抹掉、存在 localStorage 等别处,
站内跳几次后回首页会带空身份 → 调 `goHome()` 前先 `wxAuth.rememberIdentity(openid, unionid)`(数胎动 2026-10-10 首个这么做)。`wx-auth.js` 用绝对地址 `https://mp.yizhezhe.com/mama-box/js/wx-auth.js` 引入。
- 首页带 `return_url` 跳过去的应用(档案 / 婴儿房 / 商城)返回时回 `return_url` 即可(它就是当时的首页地址);**没带 `return_url` 时的兜底也要走 `goHome()`**,别 `history.back()`。
- 本仓库自己:首页内部不要用 `goHome()`(那是给别的应用回来用的);`wx-auth.js` 里 `DEFAULT_HOME` 是唯一允许写死首页的地方。
- 2026-10-09 普查(48 个目录):主返回写死 `/mama-box/`、已通知各自会话改的有 数胎动 taidong(预览版已改,正式待批)、孕检日历 chanjian-rili(10-10 已上线,复扫通过)、待产清单 daichan-qingdan、
体重 yunqi-tizhong、血糖 yunqi-xuetang、数宫缩 gongsuo-pinlv;laixin、laixin/youju、yunqi-nengbuchi、shengao-tizhong 已先调 goHome(写死仅兜底)。
新接应用前照此检查:`curl -s https://mp.yizhezhe.com/<slug>/ | grep -n "mama-box/'\|goHome"`。
## 5. 踩坑
- **不要给别的应用追加 `#openid`**:部分应用自带 hash 路由(如 `/baobao-yingji`),追加 hash 会被当非法路由 → 页面未找到。身份一律走 query(`withIdentity()` 已封装)。

File diff suppressed because one or more lines are too long

Binary file not shown.

BIN
assets/img/0022b97f8cc0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/img/0780b4698253.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/img/0dcddb83b038.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/img/0fa71babe9a2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/img/2314f334e4d0.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/img/250bb167b0f1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
assets/img/251ec778dc15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/img/2a88a5a7c1c3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8 8 32 32"><circle cx="24" cy="24" r="13.5" fill="none" stroke="#fff" stroke-width="3.2"/><path d="M24 24L30.5 17.5" stroke="#fff" stroke-width="3.2" stroke-linecap="round"/><circle cx="24" cy="24" r="3" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#FFA8C5"/><stop offset="1" stop-color="#EE6A98"/></linearGradient></defs><rect width="512" height="512" rx="120" fill="url(#g)"/><g fill="#fff"><rect x="96" y="96" width="140" height="140" rx="38"/><rect x="276" y="96" width="140" height="140" rx="38" opacity=".55"/><rect x="96" y="276" width="140" height="140" rx="38" opacity=".55"/><rect x="276" y="276" width="140" height="140" rx="38"/></g><g fill="#EE6A98"><rect x="148" y="122" width="36" height="22" rx="8"/><rect x="138" y="140" width="56" height="74" rx="20"/></g><path d="M346 300c-30 0-44 28-44 48 0 32 26 48 44 48s44-16 44-48c0-20-14-48-44-48z" fill="#EE6A98"/><circle cx="330" cy="292" r="14" fill="#EE6A98"/><circle cx="362" cy="292" r="14" fill="#EE6A98"/></svg>

After

Width:  |  Height:  |  Size: 878 B

BIN
assets/img/450581c2f05f.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
assets/img/494179795c67.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
assets/img/507e24e95ef0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/img/51e99b3060e2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

BIN
assets/img/5ae2e7f617e2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/img/5aec5c71b009.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
assets/img/5b39ca21a42e.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
assets/img/610c4d92c16d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
assets/img/681b0bdb885e.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
assets/img/6a17a98c52b8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/img/6a268e7269b7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="8.5 8 35 35" fill="#fff"><ellipse cx="26" cy="30" rx="9" ry="11.5" transform="rotate(10 26 30)"/><circle cx="14.5" cy="17" r="3.2"/><circle cx="21.5" cy="13.5" r="2.8"/><circle cx="28" cy="12.5" r="2.5"/><circle cx="34" cy="14" r="2.2"/><circle cx="38.5" cy="17.5" r="1.9"/></svg>

After

Width:  |  Height:  |  Size: 329 B

BIN
assets/img/6bdabd9eecc5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/img/6c5d82c9b093.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

BIN
assets/img/7bf792b04123.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
assets/img/83f0ab30cf36.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/img/85da5aaebb4f.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/img/91f1472ee58a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/img/a5dbb24c1ef2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="12 8 76 80"><defs><mask id="m"><rect x="0" y="0" width="100" height="100" fill="#fff"/><path d="M16 42H84" stroke="#000" stroke-width="4"/><path d="M37 60l8 8 17-17" fill="none" stroke="#000" stroke-width="6.5" stroke-linecap="round" stroke-linejoin="round"/><rect x="27" y="10" width="15" height="24" rx="7.5" fill="#000"/><rect x="58" y="10" width="15" height="24" rx="7.5" fill="#000"/></mask></defs><rect x="16" y="22" width="68" height="62" rx="14" fill="#fff" mask="url(#m)"/><rect x="30" y="12" width="9" height="18" rx="4.5" fill="#fff"/><rect x="61" y="12" width="9" height="18" rx="4.5" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 665 B

BIN
assets/img/a6f52893ad1e.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 11 35 35"><path d="M9 31c3.6 0 4.2-13 7.6-13s4 13 7.6 13 4.2-13 7.6-13 4 13 7.6 13" fill="none" stroke="#fff" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"/><circle cx="24" cy="38.5" r="2.3" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 281 B

BIN
assets/img/affcfc61f21f.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#FFC56B"/><stop offset="1" stop-color="#FF7E5C"/></linearGradient></defs><rect width="512" height="512" rx="120" fill="url(#g)"/><rect x="126" y="236" width="260" height="170" rx="26" fill="#fff"/><rect x="106" y="180" width="300" height="76" rx="24" fill="#fff"/><rect x="236" y="180" width="40" height="226" fill="#FFB25E"/><path d="M256 182c-28-62-92-66-92-30 0 26 46 32 92 30zM256 182c28-62 92-66 92-30 0 26-46 32-92 30z" fill="#fff"/><circle cx="388" cy="132" r="14" fill="#fff" opacity=".8"/><circle cx="132" cy="120" r="9" fill="#fff" opacity=".6"/></svg>

After

Width:  |  Height:  |  Size: 711 B

BIN
assets/img/b0e24b10aaee.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

BIN
assets/img/b743ed8646d5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/img/bddd81b7315d.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/img/c685f94d437c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
assets/img/c6e9c5612d8e.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/img/cceeb3b3f4ef.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/img/d41305de1db9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/img/d63b80896e8e.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
assets/img/d75b2a87e805.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/img/d953571ad48d.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/img/db2f99232b39.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
assets/img/dc4c7eea1c50.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
assets/img/dcf2ae48e0ec.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/img/dd756c516072.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

5
assets/img/dou-coin.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
assets/img/f140f4367b48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="6 6 88 88"><defs><mask id="m"><rect x="0" y="0" width="100" height="100" fill="#fff"/><circle cx="50" cy="34" r="18" fill="#000"/><path d="M26 68H74" stroke="#000" stroke-width="3" stroke-linecap="round"/></mask></defs><rect x="10" y="10" width="80" height="80" rx="22" fill="#fff" mask="url(#m)"/><line x1="38.67" y1="36.00" x2="36.21" y2="36.43" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="38.89" y1="31.02" x2="36.48" y2="30.38" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="41.19" y1="26.61" x2="39.28" y2="25.00" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="45.14" y1="23.58" x2="44.08" y2="21.31" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="50.00" y1="22.50" x2="50.00" y2="20.00" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="54.86" y1="23.58" x2="55.92" y2="21.31" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="58.81" y1="26.61" x2="60.72" y2="25.00" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="61.11" y1="31.02" x2="63.52" y2="30.38" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><line x1="61.33" y1="36.00" x2="63.79" y2="36.43" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/><path d="M50 38L50 24" stroke="#fff" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="38" r="4" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/img/fe00c2bddf9b.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/img/ff112c222b1e.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/img/glass-noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

BIN
assets/img/nursery-day.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
assets/img/qifu-tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
demo/_hero/below.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

BIN
demo/_hero/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
demo/_hero/pill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
demo/_hero/prog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
demo/_hero/week.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
demo/bump/belly.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
demo/bump/belly_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
demo/bump/below.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

396
demo/bump/index.html Normal file
View File

@@ -0,0 +1,396 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>宝宝在肚子里动 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「整块头图是妈妈的肚皮」v3。
头图纹理改为 JS 现场合成(canvas):肚皮底 belly_bg.jpg + LOGO + 倒计时 + 周数 + 进度条 + 右侧「这周的宝宝」玻璃卡。
右侧原来是胎儿插画,现在放「这周宝宝学会了 · 睁眼看光」+ 宝宝的一句悄悄话,顶出来/被摁回去时换话;
卡片也画在纹理里,所以被顶到时会和字一起鼓起来变形。
WebGL 把纹理当一层肉肉的肚皮:
· 宝宝时不时用不同部位往外顶:小脚丫(尖一点的小帐篷,顶上隐约几个脚趾)、小拳头(小而圆)、
小膝盖(长条)、小屁股(又大又缓,还会慢慢拱着挪一段);常常专挑字和卡片顶;
· 手指戳上去 = 肚皮被按下去一个窝,松手后 duang duang 回弹(欠阻尼弹簧)+ 周围一起晃;
· 戳中鼓起处 → 被摁回去,往远离手指的方向溜走,过一两秒换个部位、换个地方再顶出来;
· 头图下方「找找宝宝」收集条:戳回过哪个部位就点亮哪个。
v4:宝宝活在整个页面底下——头图 + 下面所有模块(首页截图)合成一张整页纹理,WebGL 画布固定铺满屏幕,
按滚动位置取当前这一屏来画;滑到别的模块停下来,宝宝会在这一屏里慢慢顶一下,把卡片撑成小帐篷。
?rec=1:录屏模式,由 __adv / __tap / __press 逐帧驱动(生成动图用)。 -->
<style>
html,body{margin:0;background:#F8F3EF;-webkit-tap-highlight-color:transparent;font-family:-apple-system,"PingFang SC","Noto Sans SC",sans-serif}
.page{position:relative;max-width:480px;margin:0 auto;overflow:hidden}
.page img{display:block;width:100%} /* 真实排版用的静态图:撑出页面高度,WebGL 起不来时直接看它 */
/* 整页的「肚皮」:固定铺满一屏,按滚动位置画当前这一屏 */
#cv{position:fixed;top:0;left:50%;width:min(100vw,480px);height:100%;transform:translateX(-50%);pointer-events:none;z-index:2;opacity:0}
#cv.on{opacity:1}
/* 头图下缘的「找找宝宝」收集条:和首页「记一下」同一套白色玻璃胶囊语言 */
.hunt{position:absolute;left:0;right:0;top:0;display:flex;justify-content:center;pointer-events:none;z-index:3}
.hunt .bar{margin-top:calc(min(100vw,480px) * 216 / 375);display:flex;align-items:center;gap:6px;padding:6px 7px 6px 13px;border-radius:999px;
background:rgba(255,255,255,.84);-webkit-backdrop-filter:blur(12px) saturate(160%);backdrop-filter:blur(12px) saturate(160%);
box-shadow:0 8px 20px rgba(180,80,50,.16),inset 0 0 0 1px rgba(255,255,255,.95);white-space:nowrap;
transition:transform .35s cubic-bezier(.3,1.6,.5,1)}
.hunt .bar.pop{transform:scale(1.06)}
.hunt .t{font-size:12px;font-weight:700;color:#C2563B;letter-spacing:.3px;margin-right:2px}
.hunt .c{display:flex;align-items:center;gap:3px;padding:4px 8px;border-radius:999px;font-size:11.5px;font-weight:600;
color:#D99A86;background:#FBEDE6;transition:all .3s}
.hunt .c i{width:6px;height:6px;border-radius:50%;background:#EBC3B4;transition:background .3s}
.hunt .c.on{color:#fff;background:linear-gradient(135deg,#FF9A72,#F06A45);box-shadow:0 3px 8px rgba(240,106,69,.35)}
.hunt .c.on i{background:#FFF3C4}
/* 录屏用的手指 */
.finger{position:absolute;width:38px;height:38px;margin:-19px 0 0 -19px;border-radius:50%;pointer-events:none;z-index:5;
background:radial-gradient(circle,rgba(255,255,255,.85) 0 30%,rgba(255,255,255,.35) 55%,rgba(255,255,255,0) 72%);
box-shadow:0 0 0 2px rgba(255,255,255,.7);opacity:0;transform:scale(1.25);transition:opacity .15s,transform .15s}
.finger.on{opacity:1;transform:scale(1)}
</style>
</head>
<body>
<div class="page">
<img src="belly.jpg" alt="">
<img src="below.jpg" alt="">
<i class="finger" id="finger"></i>
<div class="hunt"><div class="bar" id="hunt"><span class="t">找找宝宝</span>
<span class="c" data-k="foot"><i></i>小脚丫</span><span class="c" data-k="fist"><i></i>小拳头</span>
<span class="c" data-k="knee"><i></i>小膝盖</span><span class="c" data-k="butt"><i></i>小屁股</span></div></div>
</div>
<canvas id="cv"></canvas>
<script>
(function () {
const W = 375, H = 270, BH = 2551, HP = H + BH; // 头图 375x270(与首页一致)+ 下面模块 375x2551 = 整页
const REC = /[?&]rec=1/.test(location.search);
const page = document.querySelector('.page'), cv = document.getElementById('cv');
const hunt = document.getElementById('hunt'), finger = document.getElementById('finger');
const gl = cv.getContext('webgl', { premultipliedAlpha: false, preserveDrawingBuffer: REC, antialias: true });
if (!gl) return;
// 整页纹理尽量按 3 倍画,但不超过显卡纹理上限和 canvas 面积上限(iOS 约 1600 万像素)
const SC = Math.min(3, gl.getParameter(gl.MAX_TEXTURE_SIZE) / HP, Math.sqrt(16e6 / (W * HP)));
const VS = 'attribute vec2 a;varying vec2 v;void main(){v=a*.5+.5;v.y=1.-v.y;gl_Position=vec4(a,0.,1.);}';
const FS = `
precision highp float;
varying vec2 v;
uniform sampler2D T;
uniform vec2 S; // 整页逻辑尺寸 375xHP
uniform vec2 O; // 当前这一屏:滚动位置 y、屏幕高度(逻辑 px)
uniform vec4 F; // 鼓起处 x,y,角度,高度(px)
uniform vec4 K; // 形状 半径x,半径y,尖度(指数),脚趾显隐(0-1)
uniform vec4 G; // 左右脚(±1),脚趾蜷曲(0-1),拉长量,0
uniform vec4 P; // 手指 x,y,按压深度(负=凹下去,正=回弹鼓起),0
uniform vec2 J; // 肚皮整体晃动的位移(px)
uniform float Tm;
float sdE(vec2 p,vec2 c,vec2 r){return (length((p-c)/r)-1.)*min(r.x,r.y);}
vec2 loc(vec2 p){vec2 d=p-F.xy; float c=cos(F.z),s=sin(F.z); return vec2(c*d.x+s*d.y,-s*d.x+c*d.y);}
// 脚趾:只在小脚丫时隐约露一点,圆乎乎、很淡
float toes(vec2 p){
if(K.w<.01||F.w<.01) return 0.;
vec2 d=loc(p)/1.2; d.x*=G.x; d.y/=1.+G.z;
float cu=G.y, ht=0.;
vec3 toe[5];
toe[0]=vec3(-6.5,-22.,5.2); toe[1]=vec3(.5,-24.5,4.2); toe[2]=vec3(6.,-23.,3.8); toe[3]=vec3(10.3,-20.,3.5); toe[4]=vec3(13.4,-15.8,3.2);
for(int i=0;i<5;i++){
vec3 t=toe[i]; float w=sin(Tm*7.+float(i)*1.3)*.5+.5;
float sd=length(d-t.xy-vec2(0.,(3.+1.5*w)*cu))-t.z;
ht=max(ht,smoothstep(5.,-5.,sd)*(.5-.15*cu*w));
}
return F.w*K.w*ht;
}
// 鼓起:椭圆的软帐篷,尖度 K.z 越大顶越尖(小脚丫尖、小屁股缓)
float tent(vec2 p){
if(F.w<.01) return 0.;
vec2 d=loc(p)/K.xy; d.y/=1.+G.z*.5;
float r=sqrt(dot(d,d)+.014);
float u=clamp((1.-r)/(1.-.118),0.,1.);
return F.w*2.3*pow(u,K.z);
}
float dentR=33.;
float dent(vec2 p){vec2 d=p-P.xy;return P.z*exp(-dot(d,d)/(dentR*dentR));}
float hgt(vec2 p){return tent(p)+toes(p)*.12+dent(p);}
void main(){
vec2 p=vec2(v.x*S.x,O.x+v.y*O.y);
if(p.y<0.||p.y>S.y){gl_FragColor=vec4(.973,.953,.937,1.);return;} // iOS 橡皮筋拉出页面外
float m=1.; // 整页都是肚皮
float e=1.2;
vec2 g=vec2(hgt(p+vec2(e,0.))-hgt(p-vec2(e,0.)),hgt(p+vec2(0.,e))-hgt(p-vec2(0.,e)))/(2.*e)*m;
float h=hgt(p)*m;
// 肉感:按下去时周围的皮肤被拽向手指,回弹时被推开;整块肚皮再跟着晃
vec2 dp=p-P.xy;
float gp=exp(-dot(dp,dp)/(dentR*dentR*2.6));
float gw=exp(-dot(dp,dp)/(130.*130.));
vec2 q=p+dp*(-P.z*.03)*gp - J*gw + g*2.2;
float th=clamp(tent(p)/16.,0.,1.2);
q-=(p-F.xy)*.36*th; // 鼓起处把字往外撑开、放大:越靠近顶越被撑大
q=mix(p,q,m);
vec3 c=texture2D(T,clamp(q/S,0.,1.)).rgb;
vec3 n=normalize(vec3(-g*.85,1.));
vec3 L=normalize(vec3(-.45,-.75,.9));
float dif=dot(n,L)-dot(vec3(0.,0.,1.),L);
c+=vec3(1.,.95,.9)*max(dif,0.)*.30;
c*=1.+min(dif,0.)*vec3(.28,.36,.38); // 背光侧偏暖红,像皮肤
// 鼓起处往右下投一层淡影,让它「立」起来
vec2 d0=loc(p)/K.xy*68.-vec2(7.,11.); c*=1.-.10*exp(-dot(d0,d0)/(48.*48.))*smoothstep(1.,9.,F.w)*(1.-th)*m;
float dn=clamp(-dent(p)/10.,0.,1.)*m;
c*=1.-dn*vec3(.13,.21,.2);
vec3 Hh=normalize(L+vec3(0.,0.,1.));
c+=vec3(1.,.97,.95)*pow(max(dot(n,Hh),0.),24.)*.12*smoothstep(.5,4.,abs(h));
c+=vec3(1.,.88,.8)*smoothstep(2.,12.,h)*.05; // 顶起处微微透亮
gl_FragColor=vec4(c,1.);
}`;
function sh(t, s) { const o = gl.createShader(t); gl.shaderSource(o, s); gl.compileShader(o); if (!gl.getShaderParameter(o, gl.COMPILE_STATUS)) throw gl.getShaderInfoLog(o); return o; }
const pr = gl.createProgram();
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS)); gl.linkProgram(pr); gl.useProgram(pr);
const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
const al = gl.getAttribLocation(pr, 'a'); gl.enableVertexAttribArray(al); gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0);
const U = n => gl.getUniformLocation(pr, n);
const uO = U('O'), uS = U('S'), uF = U('F'), uK = U('K'), uG = U('G'), uP = U('P'), uJ = U('J'), uT = U('Tm');
gl.uniform2f(uS, W, HP);
/* ---------- 纹理:canvas 现场合成,宝宝卡的悄悄话会变 ---------- */
const tc = document.createElement('canvas'); tc.width = Math.floor(W * SC); tc.height = Math.floor(HP * SC);
const hc = document.createElement('canvas'); hc.width = tc.width; hc.height = Math.ceil(H * SC); // 只换头图那一截时用
const cx = tc.getContext('2d');
const SRC = { bg: 'belly_bg.jpg', top: 'top.png', pill: '../_hero/pill.png', week: '../_hero/week.png', prog: '../_hero/prog.png', below: 'below.jpg' };
const IMG = {}; let loaded = 0;
const tex = gl.createTexture(); let ready = false;
const FONT = '-apple-system,"PingFang SC","Noto Sans SC",sans-serif';
const card = { line: '我认得妈妈的声音' };
function rr(x, y, w, h, r) { cx.beginPath(); cx.moveTo(x + r, y); cx.arcTo(x + w, y, x + w, y + h, r); cx.arcTo(x + w, y + h, x, y + h, r); cx.arcTo(x, y + h, x, y, r); cx.arcTo(x, y, x + w, y, r); cx.closePath(); }
function compose(full) {
cx.setTransform(SC, 0, 0, SC, 0, 0);
if (full) cx.drawImage(IMG.below, 0, H, W, BH);
cx.drawImage(IMG.bg, 0, 0, W, H);
// 右侧原来是宝宝插画的位置:一大团很淡的暖光 + 几颗小光点,让肚皮不空
const gr = cx.createRadialGradient(292, 118, 6, 292, 118, 120);
gr.addColorStop(0, 'rgba(255,236,214,.42)'); gr.addColorStop(.55, 'rgba(255,226,200,.14)'); gr.addColorStop(1, 'rgba(255,226,200,0)');
cx.fillStyle = gr; cx.fillRect(150, 0, 225, 250);
[[226, 52, 1.6, .55], [352, 44, 1.2, .5], [214, 196, 1.3, .45], [362, 168, 1.8, .4], [246, 210, 1, .5]].forEach(([x, y, r, a]) => {
const g2 = cx.createRadialGradient(x, y, 0, x, y, r * 4); g2.addColorStop(0, 'rgba(255,255,255,' + a + ')'); g2.addColorStop(1, 'rgba(255,255,255,0)');
cx.fillStyle = g2; cx.beginPath(); cx.arc(x, y, r * 4, 0, 7); cx.fill();
});
cx.drawImage(IMG.top, 0, 0, W, 50);
cx.drawImage(IMG.pill, 14, 52, 159.69, 42);
cx.font = '900 13.5px ' + FONT; cx.textBaseline = 'middle';
cx.shadowColor = 'rgba(120,30,10,.28)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1;
cx.fillStyle = '#FFF3C4'; cx.fillText('67', 120.6, 73.5);
cx.shadowColor = 'transparent';
cx.drawImage(IMG.week, 14, 86, 110.9, 70);
cx.drawImage(IMG.prog, 14, 150, 192, 36);
// 「这周宝宝学会了」玻璃卡:30周的发育小变化 + 宝宝的一句悄悄话(被顶/被摁时换话)
const X = 228, Y = 60, CW = 134, CH = 122;
cx.save();
cx.shadowColor = 'rgba(150,55,30,.16)'; cx.shadowBlur = 18; cx.shadowOffsetY = 8;
rr(X, Y, CW, CH, 20); cx.fillStyle = 'rgba(255,255,255,.17)'; cx.fill();
cx.restore();
const hl = cx.createLinearGradient(X, Y, X, Y + CH); hl.addColorStop(0, 'rgba(255,255,255,.24)'); hl.addColorStop(1, 'rgba(255,255,255,.05)');
rr(X, Y, CW, CH, 20); cx.fillStyle = hl; cx.fill();
cx.lineWidth = 1; cx.strokeStyle = 'rgba(255,255,255,.55)'; rr(X + .5, Y + .5, CW - 1, CH - 1, 19.5); cx.stroke();
cx.shadowColor = 'rgba(120,30,10,.25)'; cx.shadowBlur = 4; cx.shadowOffsetY = 1;
cx.fillStyle = 'rgba(255,255,255,.9)'; cx.font = '600 11px ' + FONT; cx.fillText('这周宝宝学会了', X + 14, Y + 19);
cx.font = '900 25px ' + FONT; cx.fillStyle = '#fff'; cx.fillText('睁眼看光', X + 13, Y + 45);
cx.font = '600 10.5px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.9)'; cx.fillText('晒太阳时,我能感觉到亮', X + 14, Y + 68);
cx.shadowColor = 'transparent';
cx.fillStyle = 'rgba(255,255,255,.28)'; cx.fillRect(X + 14, Y + 81, CW - 28, 1);
// 悄悄话:小气泡尾巴 + 一句话
cx.fillStyle = 'rgba(255,255,255,.92)'; cx.beginPath(); cx.arc(X + 18, Y + 101, 2.6, 0, 7); cx.fill();
cx.shadowColor = 'rgba(120,30,10,.22)'; cx.shadowBlur = 3; cx.shadowOffsetY = 1;
cx.font = '600 11px ' + FONT; cx.fillStyle = 'rgba(255,255,255,.95)';
let ln = card.line; while (cx.measureText(ln).width > CW - 40 && ln.length > 2) ln = ln.slice(0, -1);
cx.fillText(ln, X + 26, Y + 101.5);
cx.shadowColor = 'transparent';
gl.bindTexture(gl.TEXTURE_2D, tex);
if (full) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, tc); return; }
// 悄悄话变了:只把头图那一截重新传上去,整页纹理很大,别每次全传
const hx = hc.getContext('2d'); hx.drawImage(tc, 0, 0);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGB, gl.UNSIGNED_BYTE, hc);
}
function setCard(line) { card.line = line; if (ready) compose(); }
Object.keys(SRC).forEach(k => {
const im = new Image(); im.onload = () => {
IMG[k] = im;
if (++loaded === Object.keys(SRC).length) {
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
ready = true; compose(true); cv.classList.add('on');
(document.fonts ? document.fonts.ready : Promise.resolve()).then(() => { compose(true); window.__ready = true; });
}
};
im.src = SRC[k];
});
// 页面 px ↔ 逻辑 px;当前这一屏在整页里的位置
let K = 1;
const view = () => { const r = page.getBoundingClientRect(); K = r.width / W; return { y: -r.top / K, h: cv.clientHeight / K }; };
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2); // 整屏都要算,最多 2 倍
cv.width = Math.round(cv.clientWidth * dpr); cv.height = Math.round(cv.clientHeight * dpr);
gl.viewport(0, 0, cv.width, cv.height);
}
resize(); addEventListener('resize', resize);
/* ---------- 宝宝的各个部位:形状不同,动法不同 ---------- */
// rx/ry 椭圆半径,sharp 尖度,toe 脚趾显隐,amp 高度,dur 停留秒数,roll 是否慢慢拱着挪
const PARTS = {
foot: { name: '小脚丫', say: ['妈妈,我在伸腿', '踢一脚打个招呼'], caught: '脚丫被你抓到啦', rx: 64, ry: 72, sharp: 1.7, toe: 1, amp: 9.5, dur: [2.4, 3.4] },
fist: { name: '小拳头', say: ['我在挥小拳头', '妈妈看我力气大'], caught: '小拳头被你握住啦', rx: 46, ry: 46, sharp: 1.9, toe: 0, amp: 8, dur: [1.8, 2.6] },
knee: { name: '小膝盖', say: ['我在翻个身', '这里有点挤哦'], caught: '膝盖被摁回去啦', rx: 50, ry: 86, sharp: 1.6, toe: 0, amp: 8.5, dur: [2.2, 3] },
butt: { name: '小屁股', say: ['我在伸懒腰~', '换个姿势睡觉'], caught: '屁股被你拍到了', rx: 100, ry: 84, sharp: 1.25, toe: 0, amp: 7, dur: [3, 3.8], roll: true },
};
const ORDER = ['foot', 'fist', 'butt', 'knee'];
const found = {};
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: 290, y: 120, ang: -.3, side: 1, part: PARTS.foot, h: { v: 0, vel: 0, to: 0, k: 140, c: 11 }, curl: 0, stretch: 0, life: 0, dur: 3, ox: 0, oy: 0 };
// 手指按压:欠阻尼,松手后过冲成一个小鼓包再回来 → duang duang
const pk = { x: 0, y: 0, d: { v: 0, vel: 0, to: 0, k: 520, c: 6.5 }, held: false };
// 肚皮整体晃:二维弹簧,阻尼更小、频率更低
const jg = { x: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 }, y: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 } };
let mode = 'wait', wait = .5, t = 0, seq = 0;
// 一半以上的概率专挑字和卡片顶(大字「30」、倒计时胶囊、进度条、slogan、宝宝卡),字会被撑起来变形
const TEXT_SPOTS = [[62, 120], [100, 74], [104, 124], [96, 166], [230, 26], [292, 104], [300, 80], [150, 168]];
// 只在当前这一屏里挑位置:头图露着就多半挑头图上的字,滑到下面就在下面的模块上顶
function pickSpot() {
const vw = view(), y0 = Math.max(vw.y + 60, 20), y1 = Math.min(vw.y + vw.h - 70, HP - 40);
const spots = TEXT_SPOTS.filter(s => s[1] > y0 && s[1] < y1);
for (let i = 0; i < 40; i++) {
let x, y;
if (spots.length && Math.random() < .7) { const s = spots[Math.floor(Math.random() * spots.length)]; x = s[0] + (Math.random() - .5) * 16; y = s[1] + (Math.random() - .5) * 10; }
else { x = 50 + Math.random() * 275; y = y0 + Math.random() * Math.max(y1 - y0, 1); }
if (Math.hypot(x - ft.x, y - ft.y) > 80) return { x, y };
}
return { x: 190, y: (y0 + y1) / 2 };
}
function appear() {
const key = ORDER[seq++ % ORDER.length], P = PARTS[key];
const s = pickSpot();
ft.key = key; ft.part = P;
ft.x = s.x; ft.y = s.y; ft.ox = ft.oy = 0;
ft.ang = (Math.random() - .5) * 1.4 + (s.x > 190 ? .25 : -.25);
ft.side = Math.random() < .5 ? 1 : -1;
ft.rollDir = s.x > 200 ? -1 : 1;
ft.h.to = P.amp; ft.h.k = P.roll ? 70 : 140; ft.h.c = P.roll ? 9 : 9; ft.h.vel += P.roll ? 30 : 120;
ft.life = 0; ft.dur = P.dur[0] + Math.random() * (P.dur[1] - P.dur[0]); ft.kicked = false;
mode = 'show';
setCard(P.say[Math.floor(Math.random() * P.say.length)]);
}
function hide(fast) {
ft.h.to = 0;
if (fast) { ft.h.k = 700; ft.h.c = 45; } else { ft.h.k = 60; ft.h.c = 14; }
mode = 'hide';
}
function press(x, y, on) {
if (on) {
pk.x = x; pk.y = y; pk.held = true; pk.d.to = -14; pk.d.k = 380; pk.d.c = 22;
const fx = ft.x + ft.ox, fy = ft.y + ft.oy, R = Math.max(ft.part.rx, ft.part.ry) * .8;
if ((mode === 'show' || (mode === 'hide' && ft.h.v > 3)) && Math.hypot(x - fx, y - fy) < R) {
// 摁中:被摁回去,往远离手指的方向溜
let dx = fx - x, dy = fy - y, l = Math.hypot(dx, dy);
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); mode = 'flee'; wait = 1.1 + Math.random() * .8;
if (navigator.vibrate) navigator.vibrate(15);
setCard(ft.part.caught);
found[ft.key] = true;
const chip = hunt.querySelector('[data-k="' + ft.key + '"]'); if (chip) chip.classList.add('on');
hunt.classList.add('pop'); setTimeout(() => hunt.classList.remove('pop'), 300);
if (Object.keys(found).length === ORDER.length) hunt.querySelector('.t').textContent = '全找到啦!';
}
} else if (pk.held) {
pk.held = false;
// 松手:弹簧变软、阻尼变小,皮肉弹回来并过冲 → duang duang
pk.d.to = 0; pk.d.k = 360; pk.d.c = 4.6; pk.d.vel += 170;
jg.y.vel += 150; jg.x.vel += (Math.random() - .5) * 90;
}
}
page.addEventListener('pointerdown', e => {
const r = page.getBoundingClientRect();
press((e.clientX - r.left) / r.width * W, (e.clientY - r.top) / r.width * W, true);
});
page.addEventListener('pointerup', () => press(0, 0, false));
// 手指一滑变成滚动:浏览器会发 pointercancel,这时窝悄悄平回去,不弹
page.addEventListener('pointercancel', () => { if (pk.held) { pk.held = false; pk.d.to = 0; pk.d.k = 300; pk.d.c = 34; } });
let lastY = 0, still = 0;
function step(dt) {
t += dt;
// 滑动停下来 0.35 秒,看看宝宝在不在这一屏里;不在就让它挪过来,在这一屏里慢慢顶一下
const vw = view();
if (Math.abs(vw.y - lastY) > 1.5) { still = 0; lastY = vw.y; }
else if (still < .35 && (still += dt) >= .35) {
const fy = ft.y + ft.oy, seen = fy > vw.y + 30 && fy < vw.y + vw.h - 30;
if (!seen) {
if (mode === 'show' || mode === 'hide') { ft.h.v = 0; ft.h.vel = 0; mode = 'wait'; wait = .5 + Math.random() * .6; }
else if (mode === 'wait') wait = Math.min(wait, .5 + Math.random() * .6);
}
}
if (mode === 'wait') { wait -= dt; if (wait <= 0) appear(); }
else if (mode === 'show') {
ft.life += dt;
const u = ft.life, P = ft.part;
if (P.roll) {
// 小屁股:慢慢拱着往一边挪
ft.ox = ft.rollDir * 30 * Math.min(u / ft.dur, 1); ft.oy = -6 * Math.sin(Math.min(u / ft.dur, 1) * Math.PI);
ft.wob = Math.sin(u * 1.2) * .08; ft.curl = 0; ft.stretch = .05 * Math.sin(u * 1.6);
} else {
ft.curl = P.toe ? Math.max(0, Math.sin(u * 3.2 - .6)) * .9 : 0;
ft.stretch = .06 * Math.sin(u * 2.4);
ft.wob = Math.sin(u * 2.1) * .12;
if (u > ft.dur * .45 && !ft.kicked) { 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 = .9 + Math.random() * 1.2; }
} else if (mode === 'flee') {
const f = ft.flee; f.t += dt;
const s = 26 * (1 - Math.exp(-f.t * 7));
ft.ox = f.dx * s; ft.oy = f.dy * s;
ft.stretch = .35 * Math.exp(-f.t * 5);
ft.curl = 1;
wait -= dt;
if (wait <= 0) appear();
}
if (mode !== 'show' && mode !== 'flee') { ft.stretch *= Math.exp(-dt * 6); }
spring(ft.h, dt); if (ft.h.v < 0) { ft.h.v = 0; ft.h.vel *= -.2; }
spring(pk.d, dt); spring(jg.x, dt); spring(jg.y, dt);
}
function draw() {
if (!ready) return;
const P = ft.part, wob = mode === 'show' ? (ft.wob || 0) : 0;
gl.uniform4f(uF, ft.x + ft.ox, ft.y + ft.oy, ft.ang + wob, ft.h.v);
gl.uniform4f(uK, P.rx, P.ry, P.sharp, P.toe);
gl.uniform4f(uG, ft.side, ft.curl, ft.stretch, 0);
gl.uniform4f(uP, pk.x, pk.y, pk.d.v, 0);
gl.uniform2f(uJ, jg.x.v, jg.y.v);
gl.uniform1f(uT, t);
const vw = view(); gl.uniform2f(uO, vw.y, vw.h);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
if (REC) {
// 录屏:由外部按帧推进
window.__adv = ms => { const n = Math.round(ms / (1000 / 120)); for (let i = 0; i < n; i++) step(1 / 120); draw(); };
window.__press = (x, y, on) => press(x, y, on);
window.__tap = (x, y) => press(x, y, true);
window.__foot = () => ({ x: ft.x + ft.ox, y: ft.y + ft.oy, h: ft.h.v, mode, key: ft.key });
window.__finger = (x, y, on) => {
finger.style.left = x * K + 'px'; finger.style.top = y * K + 'px';
finger.classList.toggle('on', !!on);
};
window.__seed = s => { let z = s; Math.random = () => ((z = (z * 16807) % 2147483647) / 2147483647); };
} else {
let last = performance.now();
(function loop(now) {
const dt = Math.min((now - last) / 1000, 1 / 20); last = now;
const n = Math.max(1, Math.round(dt * 120));
for (let i = 0; i < n; i++) step(dt / n);
draw(); requestAnimationFrame(loop);
})(last);
}
})();
</script>
</body>
</html>

BIN
demo/bump/top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

142
demo/countdown/index.html Normal file
View File

@@ -0,0 +1,142 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>宝宝把倒计时改了 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「宝宝把倒计时改了」—— 设计为每天一次的小彩蛋。
头图拆层同 demo/occupy(胶囊里的「67」单独用 HTML 渲染,才能做翻牌)。
时间线:1s 后一只小胖手从羊水囊那边伸过来 → 手指按住「67」往下拨,个位 7 滚成 6(66)→
手停住、冒个问号(发现不对)→ 偷偷往上拨回 67 → 手飞快缩回去 → 胶囊旁冒一句「我有点着急。」带一滴汗。
正式上线时按北京时间「每天首次打开首页」播一次(localStorage 记日期);本预览页每次打开都播,并给「再看一次」。
?rec=1:由 __set(t) 逐帧驱动(生成动图用)。 -->
<style>
html,body{margin:0;background:#F8F3EF;-webkit-tap-highlight-color:transparent;font-family:-apple-system,"PingFang SC","Noto Sans SC",sans-serif}
.page{position:relative;max-width:480px;margin:0 auto;overflow:hidden}
.hero-wrap{position:relative;width:100%;overflow:hidden}
.stage{position:absolute;left:0;top:0;width:375px;height:270px;transform-origin:0 0;overflow:hidden}
.stage>*{position:absolute}
.bg{left:0;top:0;width:375px;height:270px}
.pill{left:14px;top:52px;width:159.69px;height:42px}
.week{left:14px;top:86px;width:110.9px;height:70px}
.prog{left:14px;top:150px;width:192px;height:36px}
/* 「67」:十位固定,个位是一条竖排数字带,translateY 滚动 */
.num{left:120.6px;top:63px;height:20px;transform-origin:50% 60%;display:flex;font:900 13.5px/20px -apple-system,"PingFang SC","Noto Sans SC",sans-serif;
color:#FFF3C4;text-shadow:0 1px 3px rgba(120,30,10,.28)}
.num .win{display:inline-block;height:20px;overflow:hidden;position:relative;width:.62em}
.num .reel{display:flex;flex-direction:column;will-change:transform}
.num .reel i{font-style:normal;height:20px;text-align:center}
.num.hl{color:#fff}
.hand{left:0;top:0;width:150px;height:46px;transform-origin:9px 14px;will-change:transform;
filter:drop-shadow(0 0 .8px #fff) drop-shadow(0 0 .6px #fff) drop-shadow(0 3px 3px rgba(120,40,20,.28))}
.q{left:0;top:0;font:900 16px/1 -apple-system,"PingFang SC",sans-serif;color:#fff;text-shadow:0 1px 3px rgba(120,30,10,.35);opacity:0}
.bubble{left:152px;top:90px;padding:7px 12px 8px 12px;border-radius:4px 14px 14px 14px;background:#fff;white-space:nowrap;
font-size:13px;font-weight:800;color:#D9583A;letter-spacing:.3px;box-shadow:0 6px 16px rgba(150,60,30,.22);
transform-origin:0 0;opacity:0;display:flex;align-items:center;gap:5px}
.bubble::after{content:"";position:absolute;left:8px;top:-5px;border:6px solid transparent;border-bottom-color:#fff;border-top:0}
.bubble svg{width:12px;height:14px}
.below{display:block;width:100%}
.replay{position:absolute;left:50%;transform:translateX(-50%);z-index:5;border:0;padding:7px 16px;border-radius:999px;
font:600 12.5px -apple-system,"PingFang SC",sans-serif;color:#C2563B;background:rgba(255,255,255,.85);
-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 6px 16px rgba(180,80,50,.16),inset 0 0 0 1px #fff;
opacity:0;pointer-events:none;transition:opacity .3s}
.replay.on{opacity:1;pointer-events:auto}
</style>
</head>
<body>
<div class="page">
<div class="hero-wrap" id="wrap">
<div class="stage" id="stage">
<img class="bg" src="../_hero/bg.jpg" alt="">
<img class="pill" src="../_hero/pill.png" alt="">
<div class="num" id="num"><span>6</span><span class="win"><span class="reel" id="reel"><i>8</i><i>7</i><i>6</i></span></span></div>
<img class="week" src="../_hero/week.png" alt="">
<img class="prog" src="../_hero/prog.png" alt="">
<!-- 小胖手(手指朝左):前臂从右边伸进来,食指伸直,其余手指蜷着 -->
<svg class="hand" id="hand" viewBox="0 0 150 46" aria-hidden="true">
<path d="M150 14 L58 17 Q50 18 50 23 Q50 28 58 29 L150 33 Z" fill="#FFD6BF"/>
<path d="M86 18.5 q2 4.5 0 9 M96 18.2 q2 4.8 0 9.6" stroke="#F4BCA0" stroke-width="1.8" fill="none" stroke-linecap="round"/>
<ellipse cx="44" cy="25" rx="16" ry="13" fill="#FFDCC8"/>
<ellipse cx="38" cy="34" rx="7" ry="5" fill="#FFD2BA"/>
<ellipse cx="47" cy="36" rx="6.5" ry="4.6" fill="#FFD2BA"/>
<path d="M32 31 q-3 3 1 6" stroke="#F4BCA0" stroke-width="1.6" fill="none" stroke-linecap="round"/>
<path d="M33 19 L10 15" stroke="#FFDCC8" stroke-width="9" stroke-linecap="round"/>
<ellipse cx="9.5" cy="14.5" rx="2.6" ry="2.2" fill="#FFF1E8"/>
<path d="M44 13 q6 -4 10 2" stroke="#FFDCC8" stroke-width="7" fill="none" stroke-linecap="round"/>
</svg>
<div class="q" id="q">?</div>
<div class="bubble" id="bubble">
<span>我有点着急。</span>
<svg viewBox="0 0 12 14" aria-hidden="true"><path d="M6 1 C6 1 1.5 7 1.5 9.3 A4.5 4.5 0 0 0 10.5 9.3 C10.5 7 6 1 6 1z" fill="#8FD0F2"/><ellipse cx="4.6" cy="9.4" rx="1.1" ry="1.6" fill="#fff" opacity=".8"/></svg>
</div>
</div>
<button class="replay" id="replay">再看一次</button>
</div>
<img class="below" src="../_hero/below.jpg" alt="">
</div>
<script>
(function () {
const REC = /[?&]rec=1/.test(location.search);
const $ = id => document.getElementById(id);
const wrap = $('wrap'), stage = $('stage'), hand = $('hand'), reel = $('reel'), num = $('num'), q = $('q'), bubble = $('bubble'), replay = $('replay');
function fit() {
const w = Math.min(wrap.parentNode.clientWidth, 480), s = w / 375;
stage.style.transform = 'scale(' + s + ')'; wrap.style.height = 250 * s + 'px';
replay.style.top = 214 * s + 'px';
}
fit(); addEventListener('resize', fit);
const cl = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const seg = (t, a, b) => cl((t - a) / (b - a));
const lerp = (a, b, u) => a + (b - a) * u;
const eio = u => u < .5 ? 2 * u * u : 1 - Math.pow(-2 * u + 2, 2) / 2;
const eo = u => 1 - Math.pow(1 - u, 3);
const pop = u => u <= 0 ? 0 : 1 + Math.exp(-6 * u) * Math.sin(14 * u) * .35 - Math.exp(-14 * u);
const T = 8.4;
// 指尖坐标(头图坐标系)。手的 SVG 指尖在 (9,14)
const TIP_HOME = [236, 74], TIP_UP = [135, 61], TIP_DN = [135, 71];
function frame(t) {
let tip = TIP_HOME.slice(), ang = 0, digit = 1; // reel 位置:0=8,1=7,2=6
// 1.0–1.25 从羊水囊边探出一点,停一下;1.25–1.9 伸到「67」上方
if (t < 1.0) tip = [400, 74];
else if (t < 1.25) tip = [lerp(400, 250, eo(seg(t, 1.0, 1.25))), 74];
else if (t < 1.4) tip = [250, 74];
else if (t < 1.95) { const u = eio(seg(t, 1.4, 1.95)); tip = [lerp(250, TIP_UP[0], u), lerp(74, TIP_UP[1], u) - Math.sin(u * Math.PI) * 6]; }
// 1.95–2.35 往下拨:7 → 6
else if (t < 2.35) { const u = eio(seg(t, 1.95, 2.35)); tip = [TIP_UP[0], lerp(TIP_UP[1], TIP_DN[1], u)]; digit = 1 + u; ang = -8 * u; }
// 2.35–3.3 停住,看一眼……发现不对(问号、手抖两下)
else if (t < 3.3) { tip = TIP_DN.slice(); digit = 2; ang = -8; const w = seg(t, 2.8, 3.25); tip[0] += Math.sin(w * Math.PI * 6) * 1.6 * (1 - w); }
// 3.3–3.55 手指绕到下面,3.55–3.85 偷偷往上拨回 7
else if (t < 3.55) { const u = eio(seg(t, 3.3, 3.55)); tip = [lerp(TIP_DN[0], TIP_DN[0] + 2, u), lerp(TIP_DN[1], TIP_DN[1] + 3, u)]; digit = 2; ang = lerp(-8, 6, u); }
else if (t < 3.85) { const u = eio(seg(t, 3.55, 3.85)); tip = [TIP_DN[0] + 2, lerp(TIP_DN[1] + 3, TIP_UP[1], u)]; digit = 2 - u; ang = lerp(6, 10, u); }
// 3.85–4.25 飞快缩回去
else if (t < 4.3) { const u = seg(t, 3.9, 4.25); tip = [lerp(TIP_UP[0], 420, u * u), lerp(TIP_UP[1], 70, u)]; ang = lerp(10, 0, u); }
else tip = [420, 70];
hand.style.transform = 'translate(' + (tip[0] - 9).toFixed(1) + 'px,' + (tip[1] - 14).toFixed(1) + 'px) rotate(' + ang.toFixed(1) + 'deg)';
reel.style.transform = 'translateY(' + (-digit * 20).toFixed(2) + 'px)';
num.classList.toggle('hl', t > 1.95 && t < 3.85);
const hk = seg(t, 1.85, 2.05) * (1 - seg(t, 3.9, 4.15)); // 被拨的时候数字微微放大,好让人看见
num.style.transform = 'scale(' + (1 + .22 * hk).toFixed(3) + ')';
// 问号
const qi = seg(t, 2.55, 2.75), qo = seg(t, 3.25, 3.4);
q.style.opacity = Math.min(qi, 1 - qo);
q.style.transform = 'translate(' + (TIP_DN[0] + 16) + 'px,' + (38 - qi * 4) + 'px) scale(' + (.6 + .4 * pop(qi)).toFixed(3) + ')';
// 气泡 4.35 → 7.4
const bIn = seg(t, 4.35, 4.85), bOut = seg(t, 7.2, 7.45);
bubble.style.opacity = Math.min(cl(bIn * 3), 1 - bOut);
bubble.style.transform = 'scale(' + (bIn > 0 ? Math.max(0, pop(bIn * 1.5)) * (1 - bOut * .2) : 0).toFixed(3) + ')';
replay.classList.toggle('on', !REC && t > T - .4);
}
if (REC) { window.__set = frame; window.__T = T; frame(0); window.__ready = true; return; }
let start = performance.now();
(function loop(now) { frame(Math.min((now - start) / 1000, T)); requestAnimationFrame(loop); })(start);
replay.addEventListener('click', () => { start = performance.now() - 300; });
})();
</script>
</body>
</html>

211
demo/occupy/index.html Normal file
View File

@@ -0,0 +1,211 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<title>宝宝今天占领首页 · 创意预览</title>
<!-- 2026-10-03 创意预览(不进正式首页):「宝宝今天占领首页」。
头图拆成几层(背景 / 倒计时胶囊 / 「30周+3天」/ 进度条,均为首页预览版截图抠出来的透明层),
卡通宝宝用代码画(SVG)。时间线:1s 后宝宝从右下角爬出来 → 把「30周+3天」顶歪 → 跳上去坐着、戴上小皇冠 →
冒一句「今天这里归我管。」→ 几秒后跳下来爬走,数字弹回原位。
整段由 frame(t) 一个纯函数按时间算出全部位置,?rec=1 时由 __set(t) 逐帧驱动(生成动图用)。 -->
<style>
html,body{margin:0;background:#F8F3EF;-webkit-tap-highlight-color:transparent;font-family:-apple-system,"PingFang SC","Noto Sans SC",sans-serif}
.page{position:relative;max-width:480px;margin:0 auto;overflow:hidden}
.hero-wrap{position:relative;width:100%;overflow:hidden}
.stage{position:absolute;left:0;top:0;width:375px;height:270px;transform-origin:0 0;overflow:hidden}
.stage>*{position:absolute}
.bg{left:0;top:0;width:375px;height:270px}
.pill{left:14px;top:52px;width:159.69px;height:42px}
.num{left:121.6px;top:63px;width:16px;height:20px;line-height:20px;font:900 13.5px/20px -apple-system,"PingFang SC","Noto Sans SC",sans-serif;
color:#FFF3C4;text-shadow:0 1px 3px rgba(120,30,10,.28);text-align:center}
.week{left:14px;top:86px;width:110.9px;height:70px;transform-origin:14px 60px;will-change:transform}
.prog{left:14px;top:150px;width:192px;height:36px}
.baby{filter:drop-shadow(0 0 .8px #fff) drop-shadow(0 0 .8px #fff) drop-shadow(0 0 .6px #fff) drop-shadow(0 3px 3px rgba(120,40,20,.3));will-change:transform;opacity:0}
#crawl{width:104px;height:80px;left:0;top:0}
#sit{width:74px;height:81px;left:0;top:0;transform-origin:50% 80%}
.bubble{left:104px;top:30px;padding:7px 12px 8px;border-radius:14px 14px 14px 4px;background:#fff;white-space:nowrap;
font-size:13px;font-weight:800;color:#D9583A;letter-spacing:.3px;box-shadow:0 6px 16px rgba(150,60,30,.22);
transform-origin:0 100%;opacity:0}
.bubble::after{content:"";position:absolute;left:-5px;bottom:4px;border:6px solid transparent;border-right-color:#fff;border-left:0}
.below{display:block;width:100%}
.replay{position:absolute;left:50%;transform:translateX(-50%);z-index:5;border:0;padding:7px 16px;border-radius:999px;
font:600 12.5px -apple-system,"PingFang SC",sans-serif;color:#C2563B;background:rgba(255,255,255,.85);
-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 6px 16px rgba(180,80,50,.16),inset 0 0 0 1px #fff;
opacity:0;pointer-events:none;transition:opacity .3s}
.replay.on{opacity:1;pointer-events:auto}
</style>
</head>
<body>
<div class="page">
<div class="hero-wrap" id="wrap">
<div class="stage" id="stage">
<img class="bg" src="../_hero/bg.jpg" alt="">
<img class="pill" src="../_hero/pill.png" alt="">
<div class="num">67</div>
<img class="prog" src="../_hero/prog.png" alt="">
<img class="week" id="week" src="../_hero/week.png" alt="">
<!-- 爬行宝宝(侧面,头朝左)。四肢用粗圆头线段画,好绕肩/髋转动 -->
<svg class="baby" id="crawl" viewBox="0 0 130 100" aria-hidden="true">
<g stroke-linecap="round" stroke-linejoin="round" fill="none">
<g id="cLegB"><path d="M90 66 L97 88 L113 89" stroke="#EDB293" stroke-width="14"/></g>
<g id="cArmB"><path d="M47 60 L43 87" stroke="#EDB293" stroke-width="12"/></g>
</g>
<ellipse cx="72" cy="58" rx="29" ry="20" fill="#FFD9C4"/>
<path d="M58 44 q14 -6 28 0" stroke="#F7C4A9" stroke-width="3" fill="none" stroke-linecap="round"/>
<ellipse cx="95" cy="59" rx="15" ry="18" fill="#fff"/>
<path d="M85 44 q-6 15 0 30" stroke="#F3D7CB" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<circle cx="99" cy="52" r="2.2" fill="#FFC2B4"/><circle cx="104" cy="62" r="2.2" fill="#FFC2B4"/><circle cx="96" cy="67" r="2.2" fill="#FFC2B4"/>
<g stroke-linecap="round" stroke-linejoin="round" fill="none">
<g id="cLegF"><path d="M86 68 L91 91 L108 93" stroke="#FFD2BA" stroke-width="15"/></g>
<g id="cArmF"><path d="M54 62 L51 90" stroke="#FFD2BA" stroke-width="13"/></g>
</g>
<g id="cHead">
<circle cx="34" cy="40" r="25" fill="#FFDCC8"/>
<circle cx="50" cy="42" r="5.5" fill="#FFCDB4"/>
<path d="M28 16 q4 -9 12 -5 q-7 1 -6 7" fill="#8E5A44"/>
<path d="M37 15.5 q6 -5 11 1" stroke="#8E5A44" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<circle cx="21" cy="38" r="3.4" fill="#4A2A20"/><circle cx="22.2" cy="36.8" r="1.2" fill="#fff"/>
<ellipse cx="25" cy="48" rx="5.5" ry="3.8" fill="#FF9C93" opacity=".55"/>
<path d="M11 47 q4 4 8 1" stroke="#B45A42" stroke-width="2.2" fill="none" stroke-linecap="round"/>
</g>
</svg>
<!-- 坐着的宝宝(正面),一手叉腰一手举拳,头上小皇冠 -->
<svg class="baby" id="sit" viewBox="0 0 110 120" aria-hidden="true">
<g stroke-linecap="round" stroke-linejoin="round" fill="none">
<g id="sLegL"><path d="M43 90 L38 110" stroke="#FFD2BA" stroke-width="15"/></g>
<g id="sLegR"><path d="M67 90 L72 110" stroke="#FFD2BA" stroke-width="15"/></g>
</g>
<ellipse cx="37" cy="113" rx="9" ry="5.5" fill="#FFCDB4"/><ellipse cx="73" cy="113" rx="9" ry="5.5" fill="#FFCDB4"/>
<ellipse cx="55" cy="80" rx="24" ry="21" fill="#FFD9C4"/>
<path d="M31 84 q24 22 48 0 q2 14 -24 16 q-26 -2 -24 -16z" fill="#fff"/>
<path d="M32 84 q23 12 46 0" stroke="#F3D7CB" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<g stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M35 70 L22 79 L33 89" stroke="#FFD2BA" stroke-width="11"/>
<g id="sArmR"><path d="M75 70 L88 58 L90 44" stroke="#FFD2BA" stroke-width="11"/></g>
</g>
<g id="sFist"><circle cx="90" cy="41" r="7" fill="#FFCDB4"/></g>
<circle cx="55" cy="40" r="27" fill="#FFDCC8"/>
<circle cx="29" cy="42" r="5" fill="#FFCDB4"/><circle cx="81" cy="42" r="5" fill="#FFCDB4"/>
<path d="M50 14 q4 -9 12 -5 q-7 1 -6 7" fill="#8E5A44"/>
<path d="M44 41 q2.5 -4 5 0" stroke="#4A2A20" stroke-width="2.6" fill="none" stroke-linecap="round"/>
<path d="M61 41 q2.5 -4 5 0" stroke="#4A2A20" stroke-width="2.6" fill="none" stroke-linecap="round"/>
<path d="M42 33 l7 2 M68 33 l-7 2" stroke="#8E5A44" stroke-width="2" stroke-linecap="round"/>
<ellipse cx="40" cy="49" rx="5.5" ry="3.6" fill="#FF9C93" opacity=".55"/><ellipse cx="70" cy="49" rx="5.5" ry="3.6" fill="#FF9C93" opacity=".55"/>
<path d="M49 50 q6 7 12 0 z" fill="#C2563B" stroke="#C2563B" stroke-width="1.6" stroke-linejoin="round"/>
<g id="crown" transform="rotate(-10 55 12)">
<path d="M42 18 L40 4 L48 11 L55 1 L62 11 L70 4 L68 18 Z" fill="#FFD25A" stroke="#F0A93A" stroke-width="1.6" stroke-linejoin="round"/>
<circle cx="55" cy="13" r="2" fill="#F06A45"/>
</g>
</svg>
<div class="bubble" id="bubble">今天这里归我管。</div>
</div>
<button class="replay" id="replay">再看一次</button>
</div>
<img class="below" src="../_hero/below.jpg" alt="">
</div>
<script>
(function () {
const REC = /[?&]rec=1/.test(location.search);
const $ = id => document.getElementById(id);
const wrap = $('wrap'), stage = $('stage'), week = $('week'), crawl = $('crawl'), sit = $('sit'), bubble = $('bubble'), replay = $('replay');
const crown = $('crown');
function fit() {
const w = Math.min(wrap.parentNode.clientWidth, 480), s = w / 375;
stage.style.transform = 'scale(' + s + ')'; wrap.style.height = 250 * s + 'px';
replay.style.top = 214 * s + 'px';
}
fit(); addEventListener('resize', fit);
const cl = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
const seg = (t, a, b) => cl((t - a) / (b - a));
const lerp = (a, b, u) => a + (b - a) * u;
const eio = u => u < .5 ? 2 * u * u : 1 - Math.pow(-2 * u + 2, 2) / 2;
const eo = u => 1 - Math.pow(1 - u, 3);
const spring = u => u <= 0 ? 0 : 1 - Math.exp(-7 * u) * Math.cos(13 * u); // 带回弹的 0→1
const rot = (id, a, cx, cy) => $(id).setAttribute('transform', 'rotate(' + a.toFixed(2) + ' ' + cx + ' ' + cy + ')');
const pop = (u) => u <= 0 ? 0 : 1 + Math.exp(-6 * u) * Math.sin(14 * u) * .35 - Math.exp(-14 * u) * 1;
// 关键位置(逻辑像素,375 宽头图坐标)
const C0 = [384, 152], C1 = [114, 84]; // 爬行起点(右下角外)→ 顶到「30周」右侧
const SIT = [26, 33]; // 坐姿宝宝左上角(坐在「30」顶上)
const T = 9.6; // 一轮总时长
function frame(t) {
// ---- 爬进来 1.0 → 2.7 ----
let cx = C0[0], cy = C0[1], cOp = 0, flip = 1, gait = 0;
const uIn = seg(t, 1.0, 2.7);
if (t >= 1.0 && t < 3.05) {
const u = eo(uIn);
cx = lerp(C0[0], C1[0], u); cy = lerp(C0[1], C1[1], u);
cOp = 1; gait = t * 9;
if (t > 2.7) { const p = seg(t, 2.7, 2.95); cx = C1[0] - 9 * Math.sin(p * Math.PI * .5); gait = 2.7 * 9 + p * 3; } // 用头顶一下
}
// ---- 被顶歪:2.75 起数字块向左倒 -9°,带回弹 ----
let wa = 0, wx = 0;
if (t >= 2.75) {
const s = spring(seg(t, 2.75, 3.6) * 1.15);
wa = -9 * s; wx = -5 * s;
}
// ---- 跳上去 3.05 → 3.45(爬姿换坐姿,抛物线)----
let sx = SIT[0], sy = SIT[1], sOp = 0, sRot = -6, sSq = 1;
if (t >= 3.05 && t < 7.0) {
const u = seg(t, 3.05, 3.45);
const fx = C1[0] - 9 - 4, fy = C1[1] - 14;
sx = lerp(fx, SIT[0], eio(u)); sy = lerp(fy, SIT[1], eio(u)) - Math.sin(u * Math.PI) * 34;
sOp = 1;
if (u >= 1) { const q = seg(t, 3.45, 3.85); sSq = 1 - Math.exp(-8 * q) * Math.sin(q * 18) * .12; } // 落座压扁回弹
}
// ---- 坐着:晃腿、举拳 ----
const sit_t = t - 3.45;
rot('sLegL', Math.sin(t * 7) * 14, 43, 90); rot('sLegR', Math.sin(t * 7 + 2.2) * 14, 67, 90);
const fist = sit_t > 0 ? Math.sin(sit_t * 9) * 8 * Math.exp(-Math.max(0, sit_t - 1.4)) : 0;
rot('sArmR', fist, 75, 70); rot('sFist', fist, 75, 70);
const cr = seg(t, 3.55, 3.85);
crown.setAttribute('transform', 'translate(0 ' + ((1 - eo(cr)) * -16).toFixed(1) + ') rotate(-10 55 12)');
crown.style.opacity = cr;
// ---- 气泡 3.75 → 6.6 ----
const bIn = seg(t, 3.75, 4.25), bOut = seg(t, 6.45, 6.7);
bubble.style.opacity = Math.min(cl(bIn * 3), 1 - bOut);
bubble.style.transform = 'scale(' + (bIn > 0 ? Math.max(0, pop(bIn * 1.5)) * (1 - bOut * .2) : 0).toFixed(3) + ')';
// ---- 跳下来 6.75 → 7.15,再爬走 7.15 → 8.5,数字块弹回 ----
if (t >= 6.75 && t < 7.15) {
const u = seg(t, 6.75, 7.15);
sx = lerp(SIT[0], 132, eio(u)); sy = lerp(SIT[1], 112, eio(u)) - Math.sin(u * Math.PI) * 30;
sOp = 1; sRot = lerp(-6, 8, u);
}
if (t >= 7.15 && t < 8.6) {
const u = seg(t, 7.15, 8.5);
cx = lerp(128, 396, eio(u)); cy = lerp(118, 158, eio(u)); cOp = 1; flip = -1; gait = t * 10; sOp = 0;
}
if (t >= 7.0 && t < 7.15) sOp = 1;
if (t >= 6.8) {
const s = spring(seg(t, 6.8, 7.8) * 1.15);
wa = -9 * (1 - s); wx = -5 * (1 - s);
}
if (t < 3.05 || t >= 7.15) sOp = 0;
if (t >= 3.05 && t < 7.15) cOp = 0;
week.style.transform = 'translateX(' + wx.toFixed(2) + 'px) rotate(' + wa.toFixed(2) + 'deg)';
crawl.style.opacity = cOp;
const bob = Math.abs(Math.sin(gait)) * -2.4;
crawl.style.transform = 'translate(' + cx.toFixed(1) + 'px,' + (cy + bob).toFixed(1) + 'px) scaleX(' + flip + ')';
rot('cArmF', Math.sin(gait) * 20, 54, 62); rot('cArmB', -Math.sin(gait) * 20, 47, 60);
rot('cLegF', -Math.sin(gait) * 16, 86, 68); rot('cLegB', Math.sin(gait) * 16, 90, 66);
rot('cHead', Math.sin(gait * 2) * 2.5, 40, 55);
sit.style.opacity = sOp;
sit.style.transform = 'translate(' + sx.toFixed(1) + 'px,' + sy.toFixed(1) + 'px) rotate(' + sRot + 'deg) scale(' + (1 / sSq).toFixed(3) + ',' + sSq.toFixed(3) + ')';
replay.classList.toggle('on', !REC && t > T - .4);
}
if (REC) { window.__set = frame; window.__T = T; frame(0); window.__ready = true; return; }
let start = performance.now() - 0;
(function loop(now) { const t = (now - start) / 1000; frame(Math.min(t, T)); requestAnimationFrame(loop); })(start);
replay.addEventListener('click', () => { start = performance.now() - 300; });
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

2169
js/app.js

File diff suppressed because it is too large Load Diff

169
js/baby-limbs.js Normal file
View File

@@ -0,0 +1,169 @@
/* ================= 首页头图:宝宝时不时动一动小手小脚(2026-10-05) =================
用户:「右上角羊水袋里的小宝宝时不时稍微动一动小腿、小脚、小胳膊,缓缓地动,不要太激烈」。
⚠️ 之前试过 AI 生成的 GIF,整张一起扭,脸型、腿型都变(见 hero-fx.js show() 里的注释),所以这里不用动图:
在静图上盖一张 WebGL 画布,同一张 PNG 原样画上去,只在「关节 → 手脚末端」这一小段做小角度旋转扭动,
脸周围是保护圈(权重强制为 0),头和身子一像素都不动。
· 每个阶段的关节位置手工标在 LIMBS 里(坐标 = 480×480 原图像素)。1、2 阶段是胚芽,没有手脚,不动。
· 节奏:每隔 2.5~6 秒随机挑一处(小腿 / 小脚 / 小胳膊),用 1.8~2.8 秒缓缓抬起再放回;
偶尔小腿和小脚一起「蹬」一下。角度只有 3~9 度。不动的时候不重画(不占电)。
· 跟 hero-fx.js 解耦:不改它,只盯 #heroStill 的 src / class —— 换阶段、淡入淡出都自动跟上。
· 没 WebGL / 系统「减少动态效果」→ 什么都不做,静图照常。
· 调试:URL 带 limbdebug=1 → 角度 ×4、动作更频繁,并把会动的区域染成蓝色,方便核对关节标得准不准。 */
(function () {
'use strict';
const still = document.getElementById('heroStill');
const box = document.getElementById('heroBaby');
if (!still || !box || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const DEBUG = /[?&]limbdebug=1/.test(location.search);
// 每处:[关节 x, y, 末端 x, y, 影响半径, 最大角度(度), 类型] 类型 leg / foot / arm(foot 会被 leg 带着一起蹬)
// face:[圆心 x, y, 半径] —— 圈内完全不动,1.5 倍半径处才恢复
const LIMBS = {
'450581c2f05f': { face: [228, 222, 24], l: [[268, 272, 300, 286, 14, 6, 'leg'], [242, 258, 266, 240, 9, 5, 'arm']] },
'dc4c7eea1c50': { face: [240, 207, 30], l: [[288, 272, 322, 320, 18, 5, 'leg'], [306, 318, 326, 310, 10, 9, 'foot'], [228, 272, 256, 236, 10, 3, 'arm']] },
'7bf792b04123': { face: [217, 205, 32], l: [[252, 262, 292, 302, 15, 5, 'leg'], [278, 305, 296, 298, 10, 9, 'foot'], [197, 283, 236, 244, 12, 3, 'arm']] },
'f140f4367b48': { face: [220, 208, 33], l: [[262, 262, 306, 303, 15, 5, 'leg'], [288, 305, 308, 300, 10, 9, 'foot'], [200, 282, 236, 244, 12, 3, 'arm']] },
'51e99b3060e2': { face: [222, 205, 34], l: [[276, 266, 335, 310, 17, 5, 'leg'], [322, 313, 338, 305, 11, 9, 'foot'], [232, 268, 278, 240, 12, 4, 'arm']] },
'a5dbb24c1ef2': { face: [225, 215, 40], l: [[276, 252, 322, 296, 16, 4, 'leg'], [310, 305, 326, 293, 11, 9, 'foot']] },
'610c4d92c16d': { face: [250, 205, 42], l: [[292, 272, 335, 305, 16, 3, 'leg'], [325, 312, 338, 300, 12, 8, 'foot']] }
};
const N = 4; // shader 里最多 4 处
const cv = document.createElement('canvas');
cv.className = 'hero-kid hero-limb';
cv.setAttribute('aria-hidden', 'true');
still.after(cv);
const gl = cv.getContext('webgl', { premultipliedAlpha: true, alpha: true }) || cv.getContext('experimental-webgl');
if (!gl) { cv.remove(); return; }
const VS = 'attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}';
const FS = [
'precision mediump float;',
'uniform vec2 uRes;uniform sampler2D uTex;uniform vec4 uL[' + N + '];uniform vec2 uR[' + N + '];uniform vec3 uF;uniform float uDbg;',
'void main(){',
' vec2 q=vec2(gl_FragCoord.x/uRes.x,1.-gl_FragCoord.y/uRes.y)*480.;',
' vec2 off=vec2(0.);float wsum=0.;',
' for(int i=0;i<' + N + ';i++){',
' float ang=uR[i].y; if(ang==0.) continue;',
' vec2 p=uL[i].xy,d=uL[i].zw-p; float s=dot(q-p,d)/dot(d,d);',
' float dist=length(q-(p+d*clamp(s,0.,1.))); float r=uR[i].x;',
// 关节处 0 → 段长一半处 1(关节不撕开);过了末端再渐隐;离开肢体 r 以外渐隐
' float w=smoothstep(.05,.55,s)*(1.-smoothstep(1.25,1.7,s))*(1.-smoothstep(r*.6,r*1.25,dist));',
' float a=-ang*w; vec2 v=q-p;',
' off+=vec2(cos(a)*v.x-sin(a)*v.y,sin(a)*v.x+cos(a)*v.y)-v; wsum+=w;',
' }',
' float fw=smoothstep(uF.z,uF.z*1.5,length(q-uF.xy));', // 脸保护圈
' vec4 c=texture2D(uTex,(q+off*fw)/480.);',
' if(uDbg>.5) c.rgb=mix(c.rgb,vec3(.2,.5,1.)*c.a,min(wsum*fw,1.)*.6);',
' gl_FragColor=c;',
'}'
].join('\n');
function sh(t, src) { const s = gl.createShader(t); gl.shaderSource(s, src); gl.compileShader(s); return s; }
const pr = gl.createProgram();
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS)); gl.linkProgram(pr);
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) { cv.remove(); return; }
gl.useProgram(pr);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(pr, 'a');
gl.enableVertexAttribArray(aLoc); gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
const U = {}; ['uRes', 'uTex', 'uL', 'uR', 'uF', 'uDbg'].forEach(k => { U[k] = gl.getUniformLocation(pr, k); });
gl.uniform1i(U.uTex, 0); gl.uniform1f(U.uDbg, DEBUG && !/limbdebug=2/.test(location.search) ? 1 : 0); // limbdebug=2:放大角度但不染色
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
let cfg = null, ready = false, raf = 0, timer = 0, seen = true, lastPick = -1;
const moves = []; // 正在进行的动作:{ i, t0, dur, amp }
const home = document.getElementById('screen-home');
const active = () => seen && !document.hidden && (!home || home.classList.contains('is-active'));
function size() {
const d = Math.min(3, devicePixelRatio || 1), w = Math.round(Math.min(720, box.clientWidth * d)) || 480;
if (cv.width !== w) { cv.width = cv.height = w; gl.viewport(0, 0, w, w); }
gl.uniform2f(U.uRes, w, w);
}
function draw(now) {
const L = new Float32Array(N * 4), R = new Float32Array(N * 2);
let busy = false;
for (let k = moves.length - 1; k >= 0; k--) {
const m = moves[k], u = (now - m.t0) / m.dur;
if (u >= 1) { moves.splice(k, 1); continue; }
if (u < 0) { busy = true; continue; }
busy = true;
const e = Math.sin(Math.PI * u); R[m.i * 2 + 1] += m.amp * e * e; // 缓起缓落:0 → 最大 → 0
}
cfg.l.forEach((l, i) => { if (i < N) { L.set(l.slice(0, 4), i * 4); R[i * 2] = l[4]; } });
gl.uniform4fv(U.uL, L); gl.uniform2fv(U.uR, R); gl.uniform3fv(U.uF, cfg.face);
gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
return busy;
}
function loop(now) {
raf = 0;
if (!ready || !active()) return;
if (draw(now)) raf = requestAnimationFrame(loop);
}
const kick = () => { if (!raf && ready) raf = requestAnimationFrame(loop); };
// 安排下一次「动一动」
function plan() {
clearTimeout(timer);
timer = setTimeout(() => {
if (ready && active() && cfg.l.length) {
let i = Math.floor(Math.random() * cfg.l.length);
if (i === lastPick && cfg.l.length > 1) i = (i + 1) % cfg.l.length;
lastPick = i;
const now = performance.now(), l = cfg.l[i], dir = Math.random() < .5 ? -1 : 1;
const dur = 1800 + Math.random() * 1000, deg = l[5] * (.6 + Math.random() * .4) * (DEBUG ? 4 : 1);
moves.push({ i, t0: now, dur, amp: dir * deg * Math.PI / 180 });
// 小腿动时 4 成概率小脚也跟着轻轻翘一下(像蹬了一下),稍晚 0.3 秒起
const f = cfg.l.findIndex(x => x[6] === 'foot');
if (l[6] === 'leg' && f >= 0 && Math.random() < .4)
moves.push({ i: f, t0: now + 300, dur: dur - 200, amp: -dir * cfg.l[f][5] * .7 * (DEBUG ? 4 : 1) * Math.PI / 180 });
kick();
}
plan();
}, DEBUG ? 900 + Math.random() * 600 : 2500 + Math.random() * 3500);
}
// 跟着 #heroStill:换图时重新载贴图,静图淡出时画布一起淡出
function sync() {
const m = (still.getAttribute('src') || '').match(/([0-9a-f]{12})\.png/);
const next = m && LIMBS[m[1]];
if (!next) { cfg = null; ready = false; cv.classList.remove('is-on'); return; }
if (next !== cfg) {
cfg = next; ready = false; moves.length = 0;
cv.classList.remove('is-on');
const img = new Image();
img.onload = () => {
if (cfg !== next) return;
size();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
ready = true; draw(performance.now());
if (still.classList.contains('is-on')) cv.classList.add('is-on');
};
img.src = still.src;
return;
}
if (ready) cv.classList.toggle('is-on', still.classList.contains('is-on'));
}
// 调试:__limbPose(k) 把所有手脚定格在 k × 最大角度(k=0 原图),方便截图核对
if (DEBUG) window.__limbPose = k => {
clearTimeout(timer); moves.length = 0;
cfg.l.forEach((l, i) => moves.push({ i, t0: performance.now() - 500, dur: 1000, amp: k * l[5] * Math.PI / 180 }));
draw(performance.now()); moves.length = 0;
};
new MutationObserver(sync).observe(still, { attributes: true, attributeFilter: ['src', 'class'] });
sync();
plan();
addEventListener('resize', () => { if (ready) { size(); draw(performance.now()); } });
if ('IntersectionObserver' in window) new IntersectionObserver(es => { seen = es[0].isIntersecting; }).observe(box);
cv.addEventListener('webglcontextlost', () => { ready = false; cv.classList.remove('is-on'); });
})();

529
js/bump.js Normal file
View File

@@ -0,0 +1,529 @@
/* ================= 首页彩蛋:宝宝在肚皮底下动(2026-10-03,预览) =================
整个首页当成妈妈的肚皮:宝宝时不时用小脚丫 / 小拳头 / 小膝盖 / 小屁股从底下往外顶,
把顶到的那片内容撑成一个软软的小帐篷;滑到哪个模块,宝宝就跟到哪一屏。
原型见 demo/bump(那边是把整页截图当纹理扭,这里是真实页面,做法不同):
· 鼓包 = 附近真实元素(叶子级的文字块、图标、图片)按离鼓包中心的远近往外撑开 + 放大 + 往外倾斜,
再在上面盖一层只画光影的 canvas(亮面 + 投影)。元素还是原来的元素,照样能点。
· 只动 transform,不碰布局;动完把元素的行内 transform / transition 原样还回去。
和点击的关系(产品口径 2026-10-03):
1. 宝宝尽量不顶按钮:挑位置时,鼓包范围里只要压到小按钮 / 链接 / 输入框就换地方;
整张能点的大卡片(工具格、知识卡)允许顶。
2. 真顶到了能点的地方:第一下算「摁宝宝」—— 这次 click 在捕获阶段被拦掉,不跳转;
宝宝缩回去后再点就正常跳。只拦鼓包中心那一圈,点在边上照常跳。
3. 宝宝冒头不到 0.3 秒就点下去 = 用户本来就是要点按钮,碰巧冒出来:不拦,照常跳,宝宝顺便缩回去。
4. 用户在滑 / 在点的时候宝宝不出来:页面静止一会儿才顶;弹层、详情页盖住时不出来。
摁中时的反馈(一次约 1.2 秒):
0ms 轻震(安卓)+ 一声很轻的「噗」+ 按出一个窝,鼓包被压扁、往四周摊开
80ms 稍重一震 + 更轻的「啵」,鼓包往远离手指的方向滑开、沉下去,冒一颗小 ♡
松手 窝 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 周演示。
· 节奏(26 周起):有时原地「哒哒哒」带节奏连踢几拍;有时踢完不歇,旁边紧接着再踢 1~2 下
开关:「我的 → 隐私权限 → 宝宝在肚皮下动」,存 localStorage 'mb_bump'('0' = 关);系统开了「减弱动态效果」也不出来。 */
(function () {
'use strict';
const scr = document.getElementById('screen-home'), phone = document.getElementById('phone');
if (!scr || !phone || !window.requestAnimationFrame) return;
const reduce = window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches;
const isOff = () => reduce || localStorage.getItem('mb_bump') === '0';
/* ---------- 光影层:盖在首页上、在底部导航和各种弹层下面,不吃点击 ---------- */
const st = document.createElement('style');
st.textContent =
'.bump-layer{position:absolute;inset:0;z-index:3;pointer-events:none;overflow:hidden}' +
'.bump-layer canvas{position:absolute;inset:0;width:100%;height:100%}' +
'.bump-heart{position:absolute;margin:-10px 0 0 -8px;font-size:16px;line-height:20px;color:#F4593C;font-weight:700;' +
'text-shadow:0 1px 3px rgba(255,255,255,.9);animation:bumpHeart .9s cubic-bezier(.2,.9,.3,1) forwards}' +
'@keyframes bumpHeart{0%{opacity:0;transform:translateY(4px) scale(.4)}25%{opacity:1;transform:translateY(-6px) scale(1.15)}100%{opacity:0;transform:translateY(-30px) scale(.9)}}';
document.head.appendChild(st);
const layer = document.createElement('div'); layer.className = 'bump-layer'; layer.setAttribute('aria-hidden', 'true');
const cv = document.createElement('canvas'); layer.appendChild(cv);
phone.appendChild(layer);
const cx = cv.getContext('2d');
let dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.round(layer.clientWidth * dpr); cv.height = Math.round(layer.clientHeight * dpr);
}
resize(); addEventListener('resize', resize);
/* ---------- 坐标:一律用「首页内容坐标」(随内容滚动),画的时候再换成屏幕坐标 ---------- */
const scrRect = () => scr.getBoundingClientRect();
const toContent = (x, y) => { const r = scrRect(); return { x: x - r.left, y: y - r.top + scr.scrollTop }; };
/* ---------- 宝宝的各个部位:形状不同,动法不同(形状参数和 demo/bump 一致,脚趾去掉了;真实页面上停留时间加长到 3~5 秒,留够看见再伸手的时间) ---------- */
const PARTS = {
foot: { rx: 60, ry: 68, sharp: 2.5, amp: 9.5, dur: [3.6, 4.6] },
fist: { rx: 44, ry: 44, sharp: 2.7, amp: 8, dur: [3, 3.8] },
knee: { rx: 48, ry: 80, sharp: 2.3, 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: 2.6, amp: 4.5, dur: [.35, .45] }, // 被摁后不服气的反踢:小、快、摁不到
// 20~25 周:宝宝还小,只有很轻的一下(flick),偶尔稍大一点的轻碰(tap)
flick: { rx: 32, ry: 34, sharp: 2.2, amp: 6.5, dur: [.8, 1.2], soft: true },
tap: { rx: 42, ry: 42, sharp: 2.3, amp: 8, 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 }
};
// 2026-10-04 用户:「整体幅度要大一点,不然看不清、感受不到」—— 所有部位尺寸统一放大 20%(下面 apply / paint 的力度也一起加了)
// 2026-10-04 用户:「踢到的液面鼓得尖一点,更明显」—— 手脚膝的 sharp 加大约 0.6~0.8(越大越往中心收、顶越尖),paint 里顶点再加一点集中高光
const SIZE = 1.2;
for (const k in PARTS) { PARTS[k].rx *= SIZE; PARTS[k].ry *= SIZE; }
const ORDER = ['foot', 'fist', 'butt', 'knee'];
/* ---------- 孕周 → 阶段 ---------- */
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 },
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]];
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 };
const pk = { x: 0, y: 0, d: { v: 0, vel: 0, to: 0, k: 520, c: 6.5 }, held: false };
const jg = { x: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 }, y: { v: 0, vel: 0, to: 0, k: 210, c: 4.2 } };
let mode = 'wait', wait = 3, seq = 0, lastTouch = 0, lastTop = 0, still = 1, blockUntil = 0, kickTimer = 0;
/* ---------- 被撑起来的元素 ---------- */
// 叶子级元素:图片 / 图标,或者里面只有行内内容的块(文字块、小色块)。动画中的元素跳过(行内 transform 盖不过动画)
const CLICK = 'button,a,input,textarea,select,label,[role="button"],[role="switch"],[data-app],[data-go],[data-open],[data-sheet],[data-toast]';
let atoms = [];
const saved = new Map();
function isAtom(el, cs) {
if (el.ownerSVGElement) return false;
if (cs.display === 'none' || cs.display === 'inline' || cs.display === 'contents') return false;
if (cs.animationName && cs.animationName !== 'none') return false;
if (cs.position === 'fixed' || cs.position === 'sticky') return false;
const tag = el.tagName;
if (tag === 'IMG' || tag === 'svg' || tag === 'CANVAS' || tag === 'VIDEO') return true;
for (const c of el.children) { const d = getComputedStyle(c).display; if (d !== 'inline' && d !== 'none') return false; }
if (el.textContent.trim()) return true;
return cs.backgroundImage !== 'none' || !/rgba\(.*,\s*0\)|transparent/.test(cs.backgroundColor);
}
// 把鼓包 / 窝附近 R 以内的叶子元素收进来(每次冒头、每次摁时收一遍,页面数据会变)
function gather(cxp, cyp, R) {
const r0 = scrRect(), top = scr.scrollTop;
for (const el of scr.querySelectorAll('*')) {
if (saved.has(el)) continue;
const r = el.getBoundingClientRect();
if (!r.width || !r.height) continue;
const ex = r.left - r0.left + r.width / 2, ey = r.top - r0.top + top + r.height / 2;
if (Math.abs(ex - cxp) > R + r.width / 2 || Math.abs(ey - cyp) > R + r.height / 2) continue;
if (r.width * r.height > 4 * R * R) continue;
const cs = getComputedStyle(el);
if (!isAtom(el, cs)) continue;
// 大块元素整体动会很怪,按尺寸减弱
const damp = Math.min(1, (R * 1.1) / Math.max(r.width, r.height));
const a = { el, x: ex, y: ey, damp, base: cs.transform !== 'none' ? ' ' + cs.transform : '' };
saved.set(el, { t: el.style.transform, tr: el.style.transition, wc: el.style.willChange });
el.style.transition = 'none'; // 不设 will-change:设了会按原尺寸缓存图层,放大后字发糊
atoms.push(a);
}
}
// 「皮」:鼓包正下方那张卡片(有圆角或底色的容器)整体也微微鼓起来——卡片边缘一动,鼓包才看得出来
let skin = null;
function findSkin(cxp, cyp) {
const r0 = scrRect(), el = document.elementFromPoint(r0.left + cxp, r0.top + cyp - scr.scrollTop);
for (let e = el; e && e !== scr; e = e.parentElement) {
const r = e.getBoundingClientRect();
if (r.width > scr.clientWidth * 1.02 || r.height > 560) break;
if (r.width < 90 || r.height < 60) continue;
const cs = getComputedStyle(e);
if (cs.animationName !== 'none' || cs.position === 'fixed' || cs.position === 'sticky') continue;
const solid = !/rgba\(.*,\s*0\)|transparent/.test(cs.backgroundColor) || cs.backgroundImage !== 'none' || cs.boxShadow !== 'none';
if (!solid || parseFloat(cs.borderTopLeftRadius) < 6) continue;
saved.set(e, { t: e.style.transform, tr: e.style.transition, wc: e.style.willChange, to: e.style.transformOrigin });
e.style.transition = 'none';
return { el: e, x: r.left - r0.left, y: r.top - r0.top + scr.scrollTop, w: r.width, h: r.height, base: cs.transform !== 'none' ? ' ' + cs.transform : '' };
}
return null;
}
function release() {
saved.forEach((s, el) => { el.style.transform = s.t; el.style.transition = s.tr; el.style.willChange = s.wc; if ('to' in s) el.style.transformOrigin = s.to; });
saved.clear(); atoms = []; skin = null;
}
/* ---------- 挑位置:只在当前这一屏里,躲开小按钮 ---------- */
function hitOK(px, py) { // 屏幕坐标
const el = document.elementFromPoint(px, py);
if (!el || !scr.contains(el)) return null; // 被弹层 / 导航盖住,或者出了首页
const c = el.closest(CLICK);
if (c && scr.contains(c)) { const r = c.getBoundingClientRect(); if (r.width * r.height < 14000) return null; }
return el;
}
function pickSpot(P) {
const r0 = scrRect(), vh = scr.clientHeight, vw = scr.clientWidth;
const y0 = 80, y1 = vh - 130; // 不贴顶,不压底部导航
if (y1 - y0 < 120) return null;
for (let i = 0; i < 30; i++) {
const x = 60 + Math.random() * (vw - 120), y = y0 + Math.random() * (y1 - y0);
const px = r0.left + x, py = r0.top + y;
const c = hitOK(px, py); if (!c) continue;
if (i < 20 && !c.textContent.trim() && c.tagName !== 'IMG') continue; // 前 20 次只挑压着字 / 图的地方
let ok = true;
for (let k = 0; k < 8 && ok; k++) {
const a = k / 8 * Math.PI * 2;
if (!hitOK(px + Math.cos(a) * P.rx * .55, py + Math.sin(a) * P.ry * .55)) ok = false;
}
if (!ok) continue;
const p = { x, y: y + scr.scrollTop };
if (Math.hypot(p.x - ft.x, p.y - ft.y) > 80) return p;
}
return null;
}
const active = () => !!stage && !isOff() && !document.hidden && scr.classList.contains('is-active');
const idle = () => performance.now() - lastTouch > 1500;
function appear(key, at) {
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++;
if (!pk.held && Math.abs(pk.d.v) < .05) release(); // 反踢时手指的窝还在回弹,别把那几个元素放掉
ft.part = P; ft.x = s.x; ft.y = s.y; ft.ox = ft.oy = 0;
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;
// 晚期地方小:同样的部位也顶得慢、顶得闷;早期的小鱼泡泡快起快落
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.chainN = ft.chained ? (ft.chainN || 0) + 1 : 0;
// 连续踢:第一下就定好后面还跟几下;连踢的每一下都短促(1 秒出头),不像单独那一下要停好几秒
if (ft.chained) ft.chainLeft--;
else ft.chainLeft = stage && stage.chain && !P.soft && !P.roll && P !== PARTS.kick && Math.random() < stage.chain
? 1 + (Math.random() * (stage.chainMax - 1) | 0) : 0;
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;
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));
ft.dur = Math.max(ft.dur, t + .9); ft.kicked = true; // 有节奏就不再「中途再顶一下」
}
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);
mode = 'show'; run();
}
function hide(fast) {
ft.h.to = 0;
if (fast) { ft.h.k = 700; ft.h.c = 45; } else { ft.h.k = 60; ft.h.c = 14; }
mode = 'hide';
}
/* ---------- 声音:WebAudio 现场合成,不带音频文件;第一次摁的时候才建(要用户手势) ---------- */
let ac = null;
function audio() {
try {
if (!ac) {
if (navigator.audioSession) navigator.audioSession.type = 'ambient'; // 跟随静音键
const A = window.AudioContext || window.webkitAudioContext; if (!A) return null;
ac = new A();
}
if (ac.state === 'suspended') ac.resume();
} catch (e) { ac = null; }
return ac;
}
function blip(f0, f1, vol, len, delay) {
const a = audio(); if (!a) return;
const t = a.currentTime + (delay || 0), o = a.createOscillator(), g = a.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(f0, t); o.frequency.exponentialRampToValueAtTime(f1, t + len * .8);
g.gain.setValueAtTime(.0001, t); g.gain.exponentialRampToValueAtTime(vol, t + .008); g.gain.exponentialRampToValueAtTime(.0001, t + len);
o.connect(g); g.connect(a.destination); o.start(t); o.stop(t + len + .02);
}
const buzz = p => { try { navigator.vibrate && navigator.vibrate(p); } catch (e) {} };
function heart(x, y) {
const r0 = scrRect(), lr = layer.getBoundingClientRect();
const h = document.createElement('i'); h.className = 'bump-heart'; h.textContent = '♡';
h.style.left = (x + r0.left - lr.left) + 'px'; h.style.top = (y - scr.scrollTop + r0.top - lr.top) + 'px';
layer.appendChild(h); setTimeout(() => h.remove(), 950);
}
/* ---------- 摁宝宝 ---------- */
function poke(x, y) {
const fx = ft.x + ft.ox, fy = ft.y + ft.oy;
buzz([12, 70, 22]);
blip(380, 120, .2, .16); blip(560, 260, .08, .12, .08);
pk.x = x; pk.y = y; pk.held = true; pk.d.to = -12; pk.d.k = 380; pk.d.c = 22;
gather(x, y, 80);
// 鼓包先被压扁、往四周摊开,再往远离手指的方向滑走、沉下去
ft.spread.to = 1.35; ft.spread.vel += 2;
let dx = fx - x, dy = fy - y, l = Math.hypot(dx, dy);
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 = gap();
setTimeout(() => heart(fx + ft.flee.dx * 18, fy + ft.flee.dy * 18 - 10), 80);
// 三次里有一次:0.6 秒后在旁边轻轻反踢一下
clearTimeout(kickTimer);
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 = gap();
}, 600);
run();
}
function letGo(soft) {
if (!pk.held) return;
pk.held = false;
if (soft) { pk.d.to = 0; pk.d.k = 300; pk.d.c = 34; return; } // 变成滚动了:窝悄悄平回去,不弹
// 松手:弹簧变软、阻尼变小,皮肉弹回来并过冲 → duang duang,周围一起颤
pk.d.to = 0; pk.d.k = 360; pk.d.c = 4.6; pk.d.vel += 150;
jg.y.vel += 120; jg.x.vel += (Math.random() - .5) * 70;
run();
}
scr.addEventListener('pointerdown', e => {
lastTouch = performance.now();
if (isOff() || (mode !== 'show' && !(mode === 'hide' && ft.h.v > 3)) || !ft.catchable) return;
const p = toContent(e.clientX, e.clientY), fx = ft.x + ft.ox, fy = ft.y + ft.oy;
// 只认鼓包中心那一圈;点在边上照常走原来的点击
const P = ft.part, d = Math.hypot((p.x - fx) / (P.rx * .62), (p.y - fy) / (P.ry * .62));
if (d > 1) return;
if (ft.life < .3 && mode === 'show') { hide(true); return; } // 刚冒头就点 = 本来要点按钮,不拦
blockUntil = Infinity; // 这一整次手势(摁多久都算)之后的那个 click 都拦掉,松手时再收口
poke(p.x, p.y);
}, { capture: true, passive: true });
scr.addEventListener('pointerup', () => { if (blockUntil === Infinity) blockUntil = performance.now() + 400; letGo(false); }, { passive: true });
scr.addEventListener('pointercancel', () => { blockUntil = 0; letGo(true); }, { passive: true });
// 摁宝宝的那一下不算点击:捕获阶段拦掉,下面的卡片不跳转
window.addEventListener('click', e => {
if (performance.now() < blockUntil) { blockUntil = 0; e.preventDefault(); e.stopImmediatePropagation(); }
}, true);
scr.addEventListener('scroll', () => { lastTouch = performance.now(); run(); }, { passive: true });
/* ---------- 物理 ---------- */
function step(dt) {
if (mode === 'wait') {
if (active() && idle()) { wait -= dt; if (wait <= 0) appear(); }
} else if (mode === 'show') {
ft.life += dt;
const u = ft.life, P = ft.part;
if (P.roll) { // 小屁股:慢慢拱着往一边挪
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 {
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; } // 中途再顶一下
// 一拍:先往回缩一下(鼓包塌一点),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 (u > ft.dur) {
// 连续踢:这一下收得快一点,旁边紧接着再来,不等下一个间隔
ft.chainNext = ft.chainLeft > 0;
if (ft.chainNext) { ft.h.to = 0; ft.h.k = 200; ft.h.c = 24; mode = 'hide'; } else hide(false);
}
} else if (mode === 'hide') {
if (ft.h.v < .3) {
mode = 'wait'; wait = gap();
if (ft.chainNext) {
const a = Math.random() * Math.PI * 2, d = 50 + Math.random() * 40;
near = { x: ft.x + ft.ox + Math.cos(a) * d, y: ft.y + ft.oy + Math.sin(a) * d }; wait = .2 + Math.random() * .35;
}
// 小鱼吐泡泡常常是连着的:一闪没了,旁边紧跟着又一下
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));
ft.ox = f.dx * s; ft.oy = f.dy * s;
if (ft.h.v < .3) mode = 'wait';
}
// 滑动停下 0.4 秒:宝宝不在这一屏就挪过来,1 秒多后在这一屏里慢慢顶一下
const top = scr.scrollTop;
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 && 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);
}
}
spring(ft.h, dt); if (ft.h.v < 0) { ft.h.v = 0; ft.h.vel *= -.2; }
spring(ft.spread, dt);
if (mode === 'wait' && ft.spread.to !== 1 && ft.h.v < .3) { ft.spread.to = 1; ft.spread.v = 1; ft.spread.vel = 0; }
spring(pk.d, dt); spring(jg.x, dt); spring(jg.y, dt);
}
const resting = () => ft.h.v < .05 && Math.abs(ft.h.vel) < .5 && Math.abs(pk.d.v) < .05 && Math.abs(pk.d.vel) < .5 && !pk.held &&
Math.abs(jg.x.v) + Math.abs(jg.y.v) < .03 && Math.abs(jg.x.vel) + Math.abs(jg.y.vel) < .3;
/* ---------- 把物理量落到元素上 ---------- */
function tentAt(x, y) {
const P = ft.part, H = ft.h.v; if (H < .02) return 0;
let dx = x - (ft.x + ft.ox), dy = y - (ft.y + ft.oy);
const a = ft.ang + (mode === 'show' ? ft.wob : 0), c = Math.cos(a), s = Math.sin(a), sp = ft.spread.v;
const lx = (c * dx + s * dy) / (P.rx * sp), ly = (-s * dx + c * dy) / (P.ry * sp);
const r = Math.sqrt(lx * lx + ly * ly + .014), u = Math.max(0, Math.min(1, (1 - r) / (1 - .118)));
return H * 2.3 * Math.pow(u, P.sharp) / sp;
}
function apply() {
const fx = ft.x + ft.ox, fy = ft.y + ft.oy, R = Math.max(ft.part.rx, ft.part.ry) * ft.spread.v, D = pk.d.v;
if (skin) {
// 卡片以鼓包为原点轻轻放大、往上抬一点,手指按下去时反过来
const k = Math.min(ft.h.v / 9, 1.3) + Math.max(-1, Math.min(1, D / 12)) * .6;
const ox = Math.max(0, Math.min(skin.w, fx - skin.x)), oy = Math.max(0, Math.min(skin.h, fy - skin.y));
skin.el.style.transformOrigin = ox.toFixed(1) + 'px ' + oy.toFixed(1) + 'px';
skin.el.style.transform = Math.abs(k) < .005 ? skin.base.trim() :
'translateY(' + (-3.2 * k).toFixed(2) + 'px) scale(' + (1 + .045 * k).toFixed(4) + ')' + skin.base;
}
for (const a of atoms) {
let tx = 0, ty = 0, s = 1, ry = 0, rx = 0;
// 鼓包:越靠近顶越被撑开、放大,坡上的微微斜切
const th = Math.min(tentAt(a.x, a.y) / 14, 1.25) * a.damp;
if (th > .002) {
const dx = a.x - fx, dy = a.y - fy;
tx += dx * .42 * th; ty += dy * .42 * th - th * 8; s += .4 * th;
ry += 14 * Math.max(-1, Math.min(1, dx / R)) * th; rx -= 14 * Math.max(-1, Math.min(1, dy / R)) * th;
}
// 手指的窝:按下去往手指那边拽、缩小;回弹过冲时反过来
if (Math.abs(D) > .02) {
const dx = a.x - pk.x, dy = a.y - pk.y, g = Math.exp(-(dx * dx + dy * dy) / (46 * 46)) * a.damp;
tx += dx * D * .03 * g; ty += dy * D * .03 * g; s += D * .015 * g;
ry += 7 * Math.max(-1, Math.min(1, dx / 46)) * (D / 12) * g; rx -= 7 * Math.max(-1, Math.min(1, dy / 46)) * (D / 12) * g;
}
// 余波:周围一圈跟着颤
const gx = a.x - pk.x, gy = a.y - pk.y, gw = Math.exp(-(gx * gx + gy * gy) / (140 * 140));
tx -= jg.x.v * gw; ty -= jg.y.v * gw;
a.el.style.transform = (Math.abs(tx) + Math.abs(ty) + Math.abs(s - 1) * 50 + Math.abs(rx) + Math.abs(ry) < .02) ? a.base.trim() :
// 只用平面变换:带 perspective / rotateX 的 3D 变换会让文字走合成层,发糊。坡度改用轻微的斜切来暗示
'translate(' + tx.toFixed(2) + 'px,' + ty.toFixed(2) + 'px) skew(' + (ry * .5).toFixed(2) + 'deg,' + (-rx * .3).toFixed(2) + 'deg) scale(' + s.toFixed(4) + ')' + a.base;
}
}
// 光影:鼓包 = 左上亮面 + 右下投影;窝 = 左上内阴影 + 右下反光
function blob(x, y, rx, ry, ang, stops) {
cx.save(); cx.translate(x, y); cx.rotate(ang); cx.scale(rx, ry);
for (const [ox, oy, r, col] of stops) {
const g = cx.createRadialGradient(ox, oy, 0, ox, oy, r);
g.addColorStop(0, col); g.addColorStop(1, col.replace(/[\d.]+\)$/, '0)'));
cx.fillStyle = g; cx.beginPath(); cx.arc(ox, oy, r, 0, 7); cx.fill();
}
cx.restore();
}
// 脚印:脚跟 / 足弓外沿 / 前掌 / 五个脚趾,单位坐标(脚尖朝 -y),整体缩到鼓包的 0.62
const FOOT = [[0, .42, .2], [.1, .12, .15], [-.02, -.18, .24], [-.17, -.5, .1], [-.04, -.57, .08], [.07, -.55, .07], [.16, -.5, .06], [.23, -.41, .055]];
function footprint(x, y, rx, ry, ang, k) {
if (k < .25) return;
const a = (k - .25) / .75, f = .62, dark = [], lite = [];
for (const [px, py, r] of FOOT) {
// 压痕:中性灰、很淡;左上角错开一点白,像皮肤底下被顶出的浅浅轮廓,而不是画上去的图案
dark.push([px * f, py * f, r * f * 1.5, 'rgba(70,62,58,' + (.075 * a).toFixed(3) + ')']);
lite.push([px * f - .03, py * f - .04, r * f * 1.2, 'rgba(255,255,255,' + (.12 * a).toFixed(3) + ')']);
}
blob(x, y, rx, ry, ang, dark.concat(lite));
}
function paint() {
cx.setTransform(dpr, 0, 0, dpr, 0, 0);
cx.clearRect(0, 0, cv.width, cv.height);
const r0 = scrRect(), lr = layer.getBoundingClientRect(), ox = r0.left - lr.left, oy = r0.top - lr.top - scr.scrollTop;
const H = ft.h.v;
if (H > .1) {
const P = ft.part, sp = ft.spread.v, k = Math.min(H / 9, 1.3) / sp;
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) + ')'],
[.34, .42, .66, 'rgba(70,62,58,' + (.08 * k).toFixed(3) + ')'],
[-.26, -.32, .55, 'rgba(255,255,255,' + (.38 * k).toFixed(3) + ')'],
// 尖顶:顶点处一小团亮光 + 紧贴着的背光,鼓包看起来是「被顶出一个尖」而不是一片圆丘(翻身/屁股那种大片的不加)
...(P.roll ? [] : [[.07, .09, .3, 'rgba(70,62,58,' + (.09 * k).toFixed(3) + ')'],
[-.07, -.09, .2, 'rgba(255,255,255,' + (.5 * k).toFixed(3) + ')']])
]);
// 小脚丫顶上来时,隐隐约约带出一个脚印:极淡、全是软边,峰值时才看得出,凑近看像有像无
if (P === PARTS.foot || P === PARTS.kick) footprint(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));
}
const D = pk.d.v;
if (D < -.2) {
const k = Math.min(-D / 12, 1.2);
blob(pk.x + ox, pk.y + oy, 40, 40, 0, [
[-.16, -.2, .9, 'rgba(70,62,58,' + (.14 * k).toFixed(3) + ')'],
[.32, .36, .55, 'rgba(255,255,255,' + (.45 * k).toFixed(3) + ')']
]);
} else if (D > .2) {
const k = Math.min(D / 10, 1);
blob(pk.x + ox, pk.y + oy, 40, 40, 0, [
[.14, .2, .95, 'rgba(70,62,58,' + (.06 * k).toFixed(3) + ')'],
[-.22, -.28, .65, 'rgba(255,255,255,' + (.3 * k).toFixed(3) + ')']
]);
}
}
/* ---------- 循环:有东西在动才跑 rAF,静止时只用定时器数「下一次什么时候顶」 ---------- */
let running = false, last = 0, painted = false;
function frame(now) {
const dt = Math.min((now - last) / 1000, 1 / 20); last = now;
if (!active()) {
if (mode !== 'wait') { mode = 'wait'; wait = 2; }
ft.h.v = ft.h.vel = 0; pk.d.v = pk.d.vel = 0; pk.held = false; jg.x.v = jg.y.v = jg.x.vel = jg.y.vel = 0;
}
const n = Math.max(1, Math.round(dt * 120));
for (let i = 0; i < n; i++) step(dt / n);
if (mode === 'wait' && resting()) {
if (atoms.length) release();
if (painted) { cx.clearRect(0, 0, cv.width, cv.height); painted = false; }
running = false; tick(); return;
}
apply(); paint(); painted = true;
requestAnimationFrame(frame);
}
function run() { if (running || isOff()) return; running = true; last = performance.now(); requestAnimationFrame(frame); }
// 等下一次冒头:粗粒度定时器,到点再交给 rAF
let tickT = 0;
function tick() {
clearTimeout(tickT);
tickT = setTimeout(() => {
if (running) return;
if (active() && idle() && mode === 'wait') { wait -= .25; if (wait <= 0) { appear(); if (running) return; } }
tick();
}, 250);
}
// 开关切换(「隐私权限」里那一行)
addEventListener('mb-bump', () => {
if (isOff()) { mode = 'wait'; ft.h.v = pk.d.v = 0; release(); cx.clearRect(0, 0, cv.width, cv.height); }
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; }, week: () => week, at: (k, x, y) => { lastTouch = 0; appear(k, { x, y: y + scr.scrollTop }); } };
wait = 3; tick();
})();

219
js/hero-fx.js Normal file
View File

@@ -0,0 +1,219 @@
/* ================= 首页头图:按孕周换宝宝 + 阳光水波(2026-10-04) =================
用户:「宝宝按孕周换、孕囊跟着宝宝大小变;顶部往下落的花瓣去掉,换成阳光和水波纹」。
① 换宝宝:孕周来自 app.js 的 mb-preg 事件(syncPregnancy 跑完广播,首屏一次、远程档案回来再一次)。
9 个阶段各一张「羊水囊 + 宝宝」整幅图:静图(首帧)先出,GIF 加载完再盖上淡入。
没建档 / 产后 / 事件迟迟不来 → 用第 6 阶段(原来那个 25 周的宝宝)。
孕囊大小 = .hero-baby 的 --k(相对原 25 周囊的倍数),CSS 里围绕同一圆心缩放、带过渡。
调试:URL 带 kidweek=12 强制按 12 周显示。
② 羊水 + 光束:#heroWater 一张 WebGL 画布(搬自参考页 womb-water.js),在孕囊下面。
柔光束 + 柔和水波光影 + 悬浮微粒,宝宝区域避开(详见下面那段)。
不在首页 / 页面隐藏 / 头图滚出视口时停画;系统「减少动态效果」下整块不画(CSS 也隐藏)。 */
(function () {
'use strict';
// [起始孕周, 静图, GIF(null = 只用静图), 孕囊倍数 k] 静图是透明底 PNG(囊壁外渐隐),叠在羊水层上 2026-10-04:宝宝在囊里缩到原图的 70%,孕囊不变
const STAGES = [
[0, 'c685f94d437c.png', null, .42], // 1 4–7 周 胚芽 + 卵黄囊
[8, '6c5d82c9b093.png', null, .52], // 2 8–10 周 小豆芽胚胎
[11, '450581c2f05f.png', null, .62], // 3 11–13 周 刚有人形
[14, 'dc4c7eea1c50.png', null, .72], // 4 14–17 周 身子拉长、吮手
[18, '7bf792b04123.png', null, .86], // 5 18–23 周 比例接近新生儿、偏瘦
[24, 'f140f4367b48.png', null, 1], // 6 24–27 周 原来那个宝宝
[28, '51e99b3060e2.png', null, 1.01], // 7 28–31 周 大一圈、有胎发
[32, 'a5dbb24c1ef2.png', null, 1.02], // 8 32–35 周 肉嘟嘟、抱腿
[36, '610c4d92c16d.png', null, 1.03] // 9 36 周起 足月、撑满羊水囊(头朝上:10-04 用户说头朝下看着不舒服)
];
const DEFAULT = 5;
const IMG = 'assets/img/';
const box = document.getElementById('heroBaby');
const still = document.getElementById('heroStill');
const kid = document.getElementById('heroKid');
if (!box || !still || !kid) return;
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
const force = (location.search.match(/[?&]kidweek=(\d+)/) || [])[1];
function stageOf(week) {
if (week == null || isNaN(week)) return DEFAULT;
let i = 0;
while (i + 1 < STAGES.length && week >= STAGES[i + 1][0]) i++;
return i;
}
let cur = -1, seq = 0;
function show(i) {
if (i === cur) return;
cur = i;
const [, png, gif, k] = STAGES[i], my = ++seq;
box.style.setProperty('--k', k);
box.classList.toggle('is-big', STAGES[i][0] >= 24); // 24 周起孕囊大,星座牌改压在囊边(见 css .hero-baby.is-big .hero-zod)
// 先把旧图淡掉再换,避免新旧宝宝叠在一起闪一下
kid.classList.remove('is-on'); still.classList.remove('is-on');
const s = new Image();
s.onload = () => { if (my !== seq) return; still.src = s.src; still.classList.add('is-on'); };
s.src = IMG + png;
/* 2026-10-04 用户:「水波纹一出来,宝宝的脸型、腿型就变了」—— 那是 GIF 自带的整体扭动(生成动图的通病),
不是页面特效。先只用静图:宝宝一像素都不变形。gif 位传 null;以后有了只动手脚/嘴角的新动图再填回来。 */
if (reduce || !gif) return;
const g = new Image();
g.onload = () => { if (my !== seq) return; kid.src = g.src; kid.classList.add('is-on'); };
g.src = IMG + gif;
}
/* ---- 星座牌(2026-10-04,用户:「按预产期推算宝宝是哪个星座,在羊水囊外边放上『巨蟹宝宝』之类,协调、字不要太大、高级一些」) ----
挂在 .hero-baby 里(跟着孕囊一起浮),落在孕囊右下角那块空角上。
只在孕期显示(未建档 / 产后不显示:产后应按真实生日,而这里只有预产期)。
预产期离星座分界 ≤2 天时写成「巨蟹·狮子宝宝」—— 预产期本来就是估算,别说得太死。 */
const ZOD = [ // [起始月, 起始日, 名, 符号] 按起始日期排
[1, 20, '水瓶', '♒'], [2, 19, '双鱼', '♓'], [3, 21, '白羊', '♈'], [4, 20, '金牛', '♉'], [5, 21, '双子', '♊'], [6, 22, '巨蟹', '♋'],
[7, 23, '狮子', '♌'], [8, 23, '处女', '♍'], [9, 23, '天秤', '♎'], [10, 24, '天蝎', '♏'], [11, 23, '射手', '♐'], [12, 22, '摩羯', '♑']];
function zodiacOf(y, m, d) {
const t = Date.UTC(y, m - 1, d);
let i = ZOD.length - 1; // 1/1~1/19 是摩羯
for (let k = 0; k < ZOD.length; k++) if (t >= Date.UTC(y, ZOD[k][0] - 1, ZOD[k][1])) i = k;
// 摩羯跨年:1 月里的摩羯从去年 12/22 起、到今年 1/20 止;12 月里的摩羯到明年 1/20 止
const jan = i === 11 && m === 1, day = 864e5;
const start = Date.UTC(jan ? y - 1 : y, ZOD[i][0] - 1, ZOD[i][1]);
const nx = ZOD[(i + 1) % 12], nextStart = Date.UTC(i === 11 && !jan ? y + 1 : y, nx[0] - 1, nx[1]);
const near = (t - start) / day < 2 ? (i + 11) % 12 : (nextStart - t) / day <= 2 ? (i + 1) % 12 : -1;
return { main: ZOD[i], alt: near >= 0 ? ZOD[near] : null, prevAlt: near === (i + 11) % 12 };
}
const zEl = document.createElement('span');
zEl.className = 'hero-zod'; zEl.setAttribute('aria-hidden', 'true');
box.appendChild(zEl);
zEl.setAttribute('role', 'button'); zEl.removeAttribute('aria-hidden');
let zNames = [];
zEl.addEventListener('click', e => { e.stopPropagation(); if (zNames.length) window.dispatchEvent(new CustomEvent('mb-zodiac', { detail: { names: zNames } })); });
function renderZod(d) {
const due = d && !d.empty && !d.postpartum && d.due;
if (!due) { zEl.classList.remove('is-on'); return; }
const z = zodiacOf(due[0], due[1], due[2]);
/* 2026-10-04 用户:「出现『摩羯·水瓶宝宝』相当于两个星座,必须只能是一个」→ 只按预产期落在哪个区间取一个,
不再在分界 ±2 天写双星座(zodiacOf 的 alt 留着不用) */
const pair = [z.main];
// 图标:对应星座的自绘符号(js/zodiac-art.js;不用 ♋ 一类字符,部分安卓没有字形会成方框)
zEl.innerHTML = '<i>' + (window.zodiacIcon ? window.zodiacIcon(pair[0][2], 10, 2.6) : '') + '</i>' +
'<b>' + pair.map(x => x[2]).join('·') + '宝宝</b>';
zNames = pair.map(x => x[2]);
// 实体卡片按「象」上色(火 / 土 / 风 / 水,同弹窗主题;见 css .hero-zod.el-*)
zEl.className = 'hero-zod el-' + ((window.ZODIAC_ELEMENT || {})[zNames[0]] || 'fire') + ' is-on';
zEl.setAttribute('aria-label', zNames.join('·') + '宝宝,点开看看');
zEl.classList.add('is-on');
}
function onPreg(d) { show(stageOf(force != null ? +force : (d && d.week))); renderZod(d); }
addEventListener('mb-preg', e => onPreg(e.detail));
if (force != null || window.__mbPreg) onPreg(window.__mbPreg);
else setTimeout(() => { if (cur < 0) show(DEFAULT); }, 2500);
/* ---------------- 羊水 + 光束(2026-10-04 七版) ----------------
用户:「按我给的网址改头部,主要是它的光束和羊水,右上角的羊水比较好,用那一部分放到现在的头部」
参考页:beian3.douba.top/YANG-bak-10041632/?v=2(js/womb-water.js,WebGL)。
这里**原样搬了它的 fragment shader**:羊水底色 + 整体慢晃 + 折射波纹、右半边斜射的光束和焦散光纹、
背后会「呼吸」的暖光、上浮颗粒、暗角。只改两处:
· 暖光中心对齐到我们的孕囊(uC,每帧读 .hero-baby 的位置,跟着孕囊浮动走)
· 孕周从 mb-preg 事件拿(参考页读 #heroWeek 文字)
画布 #heroWater 在 .hero-baby 下面(z1),孕囊 + 宝宝(透明底 PNG)叠在上面 → 宝宝一像素都不受影响;
LOGO / 文案(z3)在最上面。之前自己写的 2D 水层、光束、光斑整段删掉(不叠两套)。
没有 WebGL → 什么都不画,CSS 背景照常;系统「减少动态效果」→ 只画一帧。 */
const cv = document.getElementById('heroWater');
if (!cv) return;
const hero = cv.parentNode, home = document.getElementById('screen-home');
const gl = cv.getContext('webgl', { antialias: false, alpha: false }) || cv.getContext('experimental-webgl');
if (!gl) return;
const VS = 'attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}';
const FS = [
'precision mediump float;',
'uniform vec2 uRes,uC;uniform float uT,uDpr,uW;',
'float h1(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
'float vn(vec2 p){vec2 i=floor(p),f=fract(p);f=f*f*(3.-2.*f);',
' return mix(mix(h1(i),h1(i+vec2(1,0)),f.x),mix(h1(i+vec2(0,1)),h1(i+vec2(1,1)),f.x),f.y);}',
'float fbm(vec2 p){float v=0.,a=.5;for(int i=0;i<4;i++){v+=a*vn(p);p=p*2.03+vec2(1.7,9.2);a*=.5;}return v;}',
'void main(){',
' vec2 p=vec2(gl_FragCoord.x,uRes.y*uDpr-gl_FragCoord.y)/uDpr;',
' vec2 q=p/uRes;float A=uRes.x/uRes.y;float t=uT;',
// 水在晃:整体慢漂 + 折射波纹
' vec2 sw=vec2(sin(t*.55)*.018+sin(t*.23)*.008,cos(t*.43)*.014);',
' vec2 fl=vec2(fbm(q*vec2(A,1.)*1.6+vec2(t*.12,0.)),fbm(q*vec2(A,1.)*1.6+vec2(5.2,-t*.10)))-.5;',
' vec2 qd=q+sw+.028*fl+.009*vec2(sin(q.y*14.+t*1.8),cos(q.x*A*10.+t*1.5));',
// 羊水底色:头图同色系,上浅下深;左边(文案区)再压一点暖色
' vec3 c=mix(vec3(.93,.64,.52),vec3(.79,.43,.36),smoothstep(-.15,1.05,qd.y));',
' c=mix(c,vec3(.84,.49,.40),.35*(1.-smoothstep(.0,.6,q.x)));',
' c+=.07*(fbm(qd*vec2(A,1.)*3.+vec2(t*.09,-t*.07))-.5);',
// 光只在右半边:左上角是 LOGO + slogan,那里一点光都不给
' float lk=smoothstep(.38,.75,q.x);',
' vec2 cp=vec2(qd.x*A*3.,qd.y*7.);',
' float a1=fbm(cp+vec2(t*.30,t*.18)),a2=fbm(cp*1.3-vec2(t*.26,-t*.22)+4.);',
' float ca=1.-smoothstep(0.,.06,abs(a1-a2));',
' c+=vec3(1.,.90,.74)*ca*pow(clamp(1.-q.y/.5,0.,1.),2.)*.22*lk;',
// 阳光:隐隐约约的几道,慢慢起伏
' vec2 r=qd-vec2(.74,-.45);float an=atan(r.x*A,r.y);',
' float ry=vn(vec2(an*7.,t*.28))*vn(vec2(an*17.,-t*.33)+3.);',
' ry=smoothstep(.15,.75,ry)*smoothstep(1.6,.2,length(r))*lk;',
' float sun=.75+.25*sin(t*.6);',
' c+=vec3(1.,.87,.66)*ry*.30*sun;',
// ── 宝宝剪影已去掉(2026-10-04 用户要求),只留原来剪影背后那团会「呼吸」的暖光,位置和浮动照旧 ──
' float m=smoothstep(14.,40.,uW);',
' vec2 bc=uC+vec2(0.,uRes.y*.03*m);',
' float bob=uRes.y*.022*sin(t*.55+.8);',
' float gd=length((p-bc-vec2(0.,bob))/uRes.y);',
' float br=.8+.2*sin(t*1.1);',
' c+=vec3(1.,.83,.62)*(.22*exp(-gd*3.2)+.10*exp(-gd*1.2))*br;',
// 上浮颗粒(只要很少几颗)
' for(int i=0;i<2;i++){float Lr=float(i);',
' vec2 g=q*vec2(A,1.)*(9.+Lr*7.)+vec2(sin(t*.4+Lr)*.3,t*(.30+Lr*.22));',
' vec2 id=floor(g),f=fract(g)-.5;float rn=h1(id+Lr*7.);',
' vec2 o=vec2(h1(id+3.1),h1(id+5.7))-.5;float dd=length(f-o*.6);',
' float tw=.6+.4*sin(t*2.+rn*30.);',
' c+=vec3(1.,.94,.84)*smoothstep(.05+.03*rn,0.,dd)*step(.80,rn)*(.35-Lr*.15)*tw;}',
' c*=1.-.12*pow(length((q-vec2(.55,.4))*vec2(1.,1.3)),2.);',
' gl_FragColor=vec4(c,1.);}'
].join('\n');
function sh(type, src) {
const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
return s;
}
let pr;
try {
pr = gl.createProgram();
gl.attachShader(pr, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(pr, sh(gl.FRAGMENT_SHADER, FS));
gl.linkProgram(pr);
if (!gl.getProgramParameter(pr, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(pr));
} catch (err) { console.warn('[hero-water]', err); return; }
gl.useProgram(pr);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const aLoc = gl.getAttribLocation(pr, 'a');
gl.enableVertexAttribArray(aLoc); gl.vertexAttribPointer(aLoc, 2, gl.FLOAT, false, 0, 0);
const U = {}; ['uRes', 'uC', 'uT', 'uDpr', 'uW'].forEach(n => { U[n] = gl.getUniformLocation(pr, n); });
const dpr = Math.min(devicePixelRatio || 1, 2), t0 = performance.now();
let W = 0, H = 0, raf = 0, seen = true, week = 20;
addEventListener('mb-preg', e => { const w = e.detail && e.detail.week; week = w > 0 ? w : 20; });
if (window.__mbPreg && window.__mbPreg.week > 0) week = window.__mbPreg.week;
function resize() {
const r = hero.getBoundingClientRect(); if (!r.width) return;
W = r.width; H = r.height;
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
gl.viewport(0, 0, cv.width, cv.height);
}
function draw(now) {
const hr = hero.getBoundingClientRect(), b = box.getBoundingClientRect();
gl.uniform2f(U.uRes, W, H); gl.uniform1f(U.uDpr, dpr);
gl.uniform2f(U.uC, b.left - hr.left + b.width / 2, b.top - hr.top + b.height / 2);
gl.uniform1f(U.uW, Math.min(42, Math.max(5, week)));
gl.uniform1f(U.uT, reduce ? 2 : (now - t0) / 1000);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
function loop(now) {
raf = 0;
if (!(seen && !document.hidden && (!home || home.classList.contains('is-active')))) return;
draw(now);
if (!reduce) raf = requestAnimationFrame(loop);
}
function kick() { if (!raf) raf = requestAnimationFrame(loop); }
resize(); kick();
addEventListener('resize', () => { resize(); kick(); });
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; if (seen) { resize(); kick(); } }).observe(hero);
if (reduce) addEventListener('mb-preg', kick);
cv.addEventListener('webglcontextlost', () => { cancelAnimationFrame(raf); cv.classList.remove('is-on'); });
cv.classList.add('is-on');
})();

62
js/zodiac-art.js Normal file
View File

@@ -0,0 +1,62 @@
/* 2026-10-04 星座宝宝的美术素材:12 个星座符号(自绘,部分安卓没有 ♋ 这类字形)、星图、四象配色与纹样。
首页小牌(js/hero-fx.js)和星座弹窗(js/app.js)共用,所以单独成文件、在 app.js 之前加载。 */
/* 星座符号(24×24,描边绘制,stroke=currentColor) */
window.ZODIAC_ICON = {
白羊: 'M12 20.5V9.5M12 9.5C12 5.5 9.8 3.6 7.6 3.6 5.6 3.6 4 5.1 4 7.1c0 1.8 1.3 3.1 3 3.1M12 9.5c0-4 2.2-5.9 4.4-5.9 2 0 3.6 1.5 3.6 3.5 0 1.8-1.3 3.1-3 3.1',
金牛: 'M4 4.2c1.2 3.4 4.2 5.4 8 5.4s6.8-2 8-5.4M12 9.6a5.2 5.2 0 1 0 0 10.4 5.2 5.2 0 0 0 0-10.4z',
双子: 'M4.5 4.2c4.6 1.6 10.4 1.6 15 0M4.5 19.8c4.6-1.6 10.4-1.6 15 0M9 5.4v13.2M15 5.4v13.2',
巨蟹: 'M5 8.6a2.4 2.4 0 1 0 4.8 0 2.4 2.4 0 0 0-4.8 0zM7.4 6.2C10.4 4.2 16 4.2 20 7.6M19 15.4a2.4 2.4 0 1 0-4.8 0 2.4 2.4 0 0 0 4.8 0zM16.6 17.8C13.6 19.8 8 19.8 4 16.4',
狮子: 'M5 15.6a3 3 0 1 0 6 0 3 3 0 0 0-6 0zM11 15.6C11 11 9.4 9.2 9.4 7.2a3.6 3.6 0 0 1 7.2 0c0 3.4-2.6 5.8-2.6 9.6 0 2 1.3 3.3 3 3.3 1.3 0 2.3-.7 2.9-1.7',
处女: 'M3.6 6.6c1.3 0 2.1.8 2.1 2.1V17M5.7 8.7c0-1.3.9-2.1 2.1-2.1s2.1.8 2.1 2.1V17M9.9 8.7c0-1.3.9-2.1 2.1-2.1s2.1.8 2.1 2.1v6.6c0 2.3 1.3 3.7 3.2 4.1M14.1 13c.9-2.3 2.7-3.3 4.1-2.6 1.6.8 1.1 3.7-1.1 5.8-1.2 1.1-2.6 1.9-3.4 2.2',
天秤: 'M4 19.2h16M4 15.4h4.4a3.8 3.8 0 1 1 7.2 0H20',
天蝎: 'M3.4 6.6c1.3 0 2.1.8 2.1 2.1V17M5.5 8.7c0-1.3.9-2.1 2.1-2.1s2.1.8 2.1 2.1V17M9.7 8.7c0-1.3.9-2.1 2.1-2.1s2.1.8 2.1 2.1v7.4c0 1.5.9 2.4 2.4 2.4h3.9M18.3 16.3l2.2 2.2-2.2 2.2',
射手: 'M5 19L19 5M12.2 5H19v6.8M7.6 11.4l5 5',
摩羯: 'M3.6 6.6c1.3 0 2.1.8 2.1 2.1V17M5.7 9c0-1.5 1-2.4 2.3-2.4S10.4 7.5 10.4 9v7c0 2 1.2 3.4 3 3.4M10.4 13.2c.9-1.9 2.4-2.9 4.2-2.9a3.1 3.1 0 1 1 0 6.2c-1.9 0-3.1-1.1-3.7-2.4',
水瓶: 'M3 10.4l3-2.6 3 2.6 3-2.6 3 2.6 3-2.6 3 2.6M3 16.2l3-2.6 3 2.6 3-2.6 3 2.6 3-2.6 3 2.6',
双鱼: 'M6.2 4c3.2 3.4 3.2 12.6 0 16M17.8 4c-3.2 3.4-3.2 12.6 0 16M4.6 12h14.8'
};
/* 星图(星座的真实连线,示意位置,0~1 归一化):[星点[x,y,亮度0~1], 连线[i,j]] —— 弹窗顶部右上角淡淡地画出来 */
window.ZODIAC_STARS = {
白羊: [[[.1,.62,.6],[.46,.42,1],[.7,.4,.8],[.84,.54,.5]], [[0,1],[1,2],[2,3]]],
金牛: [[[.5,.62,1],[.42,.52,.6],[.36,.44,.5],[.56,.5,.6],[.62,.42,.5],[.12,.16,.8],[.88,.22,.7],[.72,.78,.4]], [[0,1],[1,2],[2,5],[0,3],[3,4],[4,6],[0,7]]],
双子: [[[.3,.14,1],[.56,.12,.9],[.27,.48,.6],[.55,.48,.6],[.2,.86,.5],[.62,.86,.5],[.12,.62,.4]], [[0,2],[2,4],[1,3],[3,5],[2,3],[2,6]]],
巨蟹: [[[.5,.46,.7],[.46,.16,.6],[.24,.82,.8],[.78,.78,.6]], [[0,1],[0,2],[0,3]]],
狮子: [[[.2,.64,1],[.22,.44,.6],[.3,.28,.7],[.44,.22,.6],[.5,.34,.5],[.62,.46,.6],[.88,.52,.8],[.62,.64,.5]], [[0,1],[1,2],[2,3],[3,4],[1,5],[5,6],[6,7],[7,0]]],
处女: [[[.12,.28,.5],[.32,.36,.6],[.5,.3,.6],[.56,.52,.7],[.78,.74,1],[.4,.56,.5],[.3,.78,.5]], [[0,1],[1,2],[2,3],[3,4],[3,5],[5,6]]],
天秤: [[[.5,.14,.9],[.24,.4,.8],[.76,.44,.7],[.3,.82,.5],[.7,.86,.5]], [[0,1],[0,2],[1,2],[1,3],[2,4]]],
天蝎: [[[.14,.18,.6],[.2,.3,.6],[.1,.4,.5],[.34,.4,1],[.44,.5,.6],[.5,.66,.6],[.56,.8,.6],[.7,.88,.6],[.83,.8,.7],[.86,.66,.6]], [[0,1],[1,2],[1,3],[3,4],[4,5],[5,6],[6,7],[7,8],[8,9]]],
射手: [[[.28,.52,.7],[.44,.4,.8],[.6,.44,.7],[.7,.32,.6],[.82,.5,.8],[.66,.66,.7],[.44,.66,.6]], [[0,1],[1,2],[2,3],[2,4],[4,5],[5,6],[6,0],[6,1],[2,5]]],
摩羯: [[[.1,.3,.8],[.38,.46,.6],[.86,.24,.7],[.72,.64,.6],[.36,.76,.6]], [[0,1],[1,2],[2,3],[3,4],[4,0]]],
水瓶: [[[.1,.3,.6],[.28,.24,.7],[.44,.36,.8],[.56,.3,.5],[.62,.52,.6],[.76,.7,.6],[.88,.86,.5],[.5,.62,.5]], [[0,1],[1,2],[2,3],[2,4],[4,5],[5,6],[4,7]]],
双鱼: [[[.08,.2,.5],[.16,.14,.5],[.2,.26,.5],[.3,.38,.6],[.5,.58,.6],[.66,.84,.9],[.74,.62,.6],[.8,.42,.6],[.9,.3,.5]], [[0,1],[1,2],[2,0],[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]]]
};
/* 四象:星座 → 元素;元素 → 配色与纹样(弹窗用) */
window.ZODIAC_ELEMENT = { 白羊: 'fire', 狮子: 'fire', 射手: 'fire', 金牛: 'earth', 处女: 'earth', 摩羯: 'earth',
双子: 'air', 天秤: 'air', 水瓶: 'air', 巨蟹: 'water', 天蝎: 'water', 双鱼: 'water' };
window.ZODIAC_THEME = {
fire: { name: '火象星座', words: '热情 · 勇敢 · 有行动力', band: 'linear-gradient(160deg,#FFA275 0%,#F0704A 48%,#C8443A 100%)', body: '#FFF7F2', ink: '#D9583C' },
earth: { name: '土象星座', words: '踏实 · 温和 · 很靠得住', band: 'linear-gradient(160deg,#D8C092 0%,#AE9263 50%,#77704C 100%)', body: '#FAF6EE', ink: '#8E7448' },
air: { name: '风象星座', words: '灵动 · 聪明 · 爱交朋友', band: 'linear-gradient(160deg,#CFE3F7 0%,#A3BDEC 50%,#8C93DC 100%)', body: '#F5F7FD', ink: '#6A79CC' },
water: { name: '水象星座', words: '温柔 · 细腻 · 很有感知力', band: 'linear-gradient(160deg,#86D3DE 0%,#4C9FC6 50%,#33669F 100%)', body: '#F2F8FB', ink: '#2F7FAD' }
};
/* 头部底边纹样:最下一层用正文底色填,头部就「长」进正文里;上面两层半透明白做层次。viewBox 0 0 375 56 */
window.ZODIAC_MOTIF = {
fire: ['M0 56V40 C10.4 40.0 13.8 30.1 18.8 22.0 C21.8 31.0 33.3 40.0 41.7 40.0 C52.1 40.0 58.5 23.6 63.5 10.1 C66.5 25.1 75.0 40.0 83.3 40.0 C93.8 40.0 92.9 28.4 97.9 18.9 C100.9 29.5 116.7 40.0 125.0 40.0 C135.4 40.0 141.2 24.0 146.2 10.9 C149.2 25.4 158.3 40.0 166.7 40.0 C177.1 40.0 181.3 26.8 186.3 16.1 C189.3 28.0 200.0 40.0 208.3 40.0 C218.7 40.0 218.4 24.8 223.4 12.4 C226.4 26.2 241.7 40.0 250.0 40.0 C260.4 40.0 267.7 25.5 272.7 13.6 C275.7 26.8 283.3 40.0 291.7 40.0 C302.1 40.0 303.8 26.0 308.8 14.6 C311.8 27.3 325.0 40.0 333.3 40.0 C343.8 40.0 345.3 24.4 350.3 11.7 C353.3 25.8 366.7 40.0 375.0 40.0 V56z',
'M0 56V46 C8.5 46.0 13.9 35.2 18.6 26.3 C21.5 36.1 27.3 46.0 34.1 46.0 C42.6 46.0 43.2 38.8 47.9 32.9 C50.8 39.4 61.4 46.0 68.2 46.0 C76.7 46.0 77.5 35.0 82.2 26.0 C85.1 36.0 95.5 46.0 102.3 46.0 C110.8 46.0 116.0 38.9 120.8 33.1 C123.7 39.5 129.5 46.0 136.4 46.0 C144.9 46.0 143.9 35.1 148.7 26.2 C151.6 36.1 163.6 46.0 170.5 46.0 C179.0 46.0 181.8 37.8 186.6 31.1 C189.4 38.5 197.7 46.0 204.5 46.0 C213.1 46.0 217.1 35.5 221.8 27.0 C224.7 36.5 231.8 46.0 238.6 46.0 C247.2 46.0 245.8 36.8 250.6 29.3 C253.5 37.6 265.9 46.0 272.7 46.0 C281.2 46.0 285.9 36.2 290.6 28.2 C293.5 37.1 300.0 46.0 306.8 46.0 C315.3 46.0 317.4 36.0 322.1 27.8 C325.0 36.9 334.1 46.0 340.9 46.0 C349.4 46.0 349.0 37.1 353.7 29.8 C356.6 37.9 368.2 46.0 375.0 46.0 V56z',
'M0 56V50c30-3 60-6 94-4s62 5 94 3 62-6 94-4 62 5 93 3V56z'],
earth: ['M0 56V30l38-18 30 14 44-24 36 20 28-10 46 26 34-16 40 18 30-12 49 22V56z',
'M0 56V40l46-14 34 10 40-16 42 16 30-6 42 14 38-12 36 12 34-8 33 12V56z',
'M0 56V48c40-6 80-8 125-4s90 6 130 0 80-6 120-2V56z'],
air: ['M0 56V38c18 0 22-14 40-14s22 12 38 12 18-16 38-16 24 14 40 14 20-12 38-12 22 14 40 14 20-14 38-14 22 12 40 12 18-10 30-10 18 10 33 10V56z',
'M0 56V44c22 0 30-10 52-10s28 10 48 10 28-12 50-12 30 12 50 12 28-10 50-10 30 10 50 10 26-8 42-8 22 8 33 8V56z',
'M0 56V49c30 0 50-5 80-5s48 6 78 6 50-6 80-6 48 5 78 5 40-4 59-4V56z'],
water: ['M0 56V36c22 0 26-12 47-12s25 12 47 12 25-12 47-12 25 12 47 12 25-12 47-12 25 12 47 12 25-12 47-12 25 12 46 12V56z',
'M0 56V43c24 0 28-9 50-9s27 9 50 9 27-9 50-9 27 9 50 9 27-9 50-9 27 9 50 9 27-9 50-9 26 9 25 9V56z',
'M0 56V49c26 0 30-6 54-6s28 6 54 6 28-6 54-6 28 6 54 6 28-6 54-6 28 6 54 6 26-5 51-5V56z']
};
window.zodiacIcon = function (n, w, sw) {
const d = window.ZODIAC_ICON[n]; if (!d) return '';
return '<svg viewBox="0 0 24 24" width="' + w + '" height="' + w + '" fill="none" stroke="currentColor" stroke-width="' + (sw || 2) + '" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path fill="none" d="' + d + '"/></svg>';
};

File diff suppressed because it is too large Load Diff

1317
night.css Normal file

File diff suppressed because it is too large Load Diff

220
tools/gen-night.py Normal file
View File

@@ -0,0 +1,220 @@
#!/usr/bin/env python3
"""夜晚模式样式生成器:读 mama-box.css,把浅色底 / 深色字自动翻成夜色,写进 night.css 的「自动生成」段。
为什么用生成而不是手写:mama-box.css 有 4700 行、几百处写死的 #fff / 暖白,手写一份夜间版
既漏得多、又会和日间版各改各的。这里只做「颜色翻转」这一件机械的事:
· 背景 / 边框类属性里,亮且不太艳的颜色(暖白、米色、浅灰线)→ 同色相的深色(卡片比页面底略亮);
· color / fill / stroke 里,暗的颜色(正文墨色、灰字)→ 同色相的浅色;
· 品牌橙、工具卡的绿 / 紫 / 蓝这些高饱和色、白字、透明度低的玻璃白,一律不动。
每条规则原样复制选择器、前面加 html.night,所以优先级比日间版恰好高一级,顺序关系不变。
自动翻不好的地方写在 night.css 的「手调」段(在生成段之后,同优先级后写的赢)。
用法:python3 tools/gen-night.py [mama-box.css] 改了 mama-box.css 的颜色后重跑一次。
"""
import re, sys, colorsys, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent
SRC = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / 'mama-box.css'
OUT = ROOT / 'night.css'
MARK_A = '/* ==== 自动生成段:tools/gen-night.py 写入,别手改 ==== */'
MARK_B = '/* ==== 自动生成段结束 ==== */'
BG_PROPS = {'background', 'background-color', 'background-image', 'border', 'border-color', 'border-top',
'border-bottom', 'border-left', 'border-right', 'border-top-color', 'border-bottom-color',
'border-left-color', 'border-right-color', 'outline', 'outline-color', 'box-shadow', 'caret-color'}
FG_PROPS = {'color', 'fill', 'stroke', '-webkit-text-fill-color', 'text-decoration-color'}
SKIP_SEL = re.compile(r'home-hero|hero-|\.hi-|\.hd-|\.letter-card|zd-|ar-hero|page-hero|ny-peek|nyp-|::selection|\.skip')
COL = re.compile(r'#([0-9a-fA-F]{3,8})\b|rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+%?)\s*)?\)')
def parse_hex(h):
if len(h) in (3, 4):
h = ''.join(c * 2 for c in h)
r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
a = int(h[6:8], 16) / 255 if len(h) == 8 else 1
return r, g, b, a
def fmt(r, g, b, a):
R, G, B = (round(max(0, min(1, x)) * 255) for x in (r, g, b))
return f'#{R:02X}{G:02X}{B:02X}' if a >= .999 else f'rgba({R},{G},{B},{round(a, 3)})'
NIGHT_H = 280 / 360
def bg_map(r, g, b, a):
h, l, s = colorsys.rgb_to_hls(r, g, b)
if l < .80: # 艳色 / 中间调:品牌色、工具卡底,不动(浅粉浅黄这类高饱和的淡色照样翻)
return None
if a < .2: # 几乎透明的白:只是渐变收尾,不动
return None
nl = max(.115, .17 - (1 - l) * .5) # #fff→.17(卡片) 米色浅底 .93→.135 浅线→.12;页底另由 --bg 定
if s < .3: # 近中性的暖白 / 灰:统一染成夜色的一点紫,不然一片死灰
h, ns = NIGHT_H, .10
else:
ns = min(s, .5) * .45
return fmt(*colorsys.hls_to_rgb(h, nl, ns), a)
def fg_map(r, g, b, a):
h, l, s = colorsys.rgb_to_hls(r, g, b)
if l > .55 or (s > .55 and l > .3): # 已经够亮 / 艳色强调字,不动
return None
nl = min(.93, .95 - l * .55) # 墨色 .15→.87 灰字 .42→.72
ns = min(s, .3) * .5
if s < .3:
h, ns = 25 / 360, .12 # 字色带一点暖,跟日间的暖墨色同一气质
return fmt(*colorsys.hls_to_rgb(h, nl, ns), a)
def vivid(val):
"""这条背景里有没有「主色」(品牌橙、工具卡的绿紫蓝):有就整条不动,否则渐变里的白高光被翻黑、主色还在,成了脏灰。"""
for m in COL.finditer(val):
if m.group(1):
if len(m.group(1)) not in (3, 4, 6, 8):
continue
r, g, b, a = parse_hex(m.group(1))
else:
r, g, b = (float(m.group(i)) / 255 for i in (2, 3, 4))
a = m.group(5); a = 1 if a is None else (float(a[:-1]) / 100 if a.endswith('%') else float(a))
h, l, s = colorsys.rgb_to_hls(r, g, b)
if a > .5 and s > .45 and .3 < l < .78:
return True
return False
def recolor(val, fn):
changed = False
def sub(m):
nonlocal changed
if m.group(1):
if len(m.group(1)) not in (3, 4, 6, 8):
return m.group(0)
c = parse_hex(m.group(1))
else:
a = m.group(5)
a = 1 if a is None else (float(a[:-1]) / 100 if a.endswith('%') else float(a))
c = (float(m.group(2)) / 255, float(m.group(3)) / 255, float(m.group(4)) / 255, a)
n = fn(*c)
if n is None:
return m.group(0)
changed = True
return n
out = COL.sub(sub, val)
return out if changed else None
def map_keyword(prop, val):
v = val.strip().lower().replace('!important', '').strip()
if prop in BG_PROPS and v in ('white',):
return val.lower().replace('white', '#2A2530')
return None
def strip_comments(s):
return re.sub(r'/\*.*?\*/', '', s, flags=re.S)
def split_decls(body):
out, depth, cur = [], 0, ''
for ch in body:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if ch == ';' and depth == 0:
out.append(cur); cur = ''
else:
cur += ch
out.append(cur)
return [d.strip() for d in out if d.strip()]
def prefix_sel(sel):
parts, depth, cur = [], 0, ''
for ch in sel:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if ch == ',' and depth == 0:
parts.append(cur); cur = ''
else:
cur += ch
parts.append(cur)
res = []
for p in parts:
p = p.strip()
if p.startswith(':root'):
continue
if re.match(r'html\b', p):
p = 'html.night' + p[4:]
else:
p = 'html.night ' + p
res.append(p)
return ','.join(res)
def walk(css):
"""返回 [(prelude 或 None, 内容)]。嵌套 @media / @supports 递归,@keyframes / @font-face 跳过。"""
i, n, out = 0, len(css), []
while i < n:
j = css.find('{', i)
if j < 0:
break
pre = css[i:j].strip()
depth, k = 1, j + 1
while k < n and depth:
if css[k] == '{':
depth += 1
elif css[k] == '}':
depth -= 1
k += 1
body = css[j + 1:k - 1]
i = k
if pre.startswith('@'):
if re.match(r'@(media|supports)', pre):
inner = walk(body)
if inner:
out.append(pre + '{\n' + '\n'.join(inner) + '\n}')
continue
# ⚠️ 所有规则的**所有**颜色声明(含没翻的)都抄一份:
# 日间版里后写的规则会盖掉先写的;只抄翻过色的那几条,先写的规则加了 html.night 反而赢过后写的(踩过:工具卡变灰)。
# 同理,头图那些不翻色的规则也照抄,免得被前面翻过色的通用规则压住。
skip = bool(SKIP_SEL.search(pre))
changed = skip
decls = []
for d in split_decls(body):
if ':' not in d or d.startswith('--'):
continue
prop, val = d.split(':', 1)
prop = prop.strip().lower()
if prop not in BG_PROPS and prop not in FG_PROPS:
continue
nv = None
if not skip and prop in BG_PROPS and not vivid(val):
nv = recolor(val, bg_map) or map_keyword(prop, val)
elif not skip and prop in FG_PROPS:
nv = recolor(val, fg_map)
if nv:
changed = True
decls.append(f'{prop}:{(nv or val).strip()}')
sel = prefix_sel(pre)
if decls and sel: # 没翻色的也抄:它可能是在覆盖前面某条翻过色的规则(工具卡彩色底就是这么丢的)
out.append(sel + '{' + ';'.join(decls) + '}')
return out
rules = walk(strip_comments(SRC.read_text()))
gen = MARK_A + '\n' + '\n'.join(rules) + '\n' + MARK_B
old = OUT.read_text() if OUT.exists() else ''
if MARK_A in old:
a, b = old.index(MARK_A), old.index(MARK_B) + len(MARK_B)
new = old[:a] + gen + old[b:]
else:
new = old + '\n' + gen + '\n'
OUT.write_text(new)
print(f'{len(rules)} 条规则 → {OUT}')

13
tools/gen-nursery-sky.md Normal file
View File

@@ -0,0 +1,13 @@
# 婴儿房封面补天空(2026-10-06)
起因:首页「我们的家」视差缝往上滑时,原封面(nursery-day/night.jpg,1125×1500)顶边正好切在松树中间,
树被一刀截断、上面是一块纯色。用户要求「把树上面整体调成天空,衔接过渡一下」。
做法(产出 assets/img/nursery-{day,night}-sky.jpg,1125×2000):
1. 原图贴到 1125×2000 画布底部,上方 500 像素填原图顶部天色(白天 #F0F5F7、夜晚 #2F375A)。
2. 用图片生成技能(tupian,模型 nano_banana_pro,9:16 2k)图生图:只补上方空白——树尖补完整,往上是天空(白天淡云,夜晚小星星),其余不变。
3. AI 结果直接缩放到 1125×2000(实测和原图逐像素对齐,无需平移),y<600 用 AI、600–760 smoothstep 渐变、y>760 用原图像素。
4. JPG quality 88。
3D 婴儿房会话(33d6c837)以后若重新导出封面:照上面四步重做一遍,改 app.js 的 NY_COVER_V 和 index.html 的 ?v=。
旧的 nursery-day.jpg / nursery-night.jpg 保留没删,回滚时把 -sky 去掉、CSS 里 margin-top:-136px 那条删掉即可。