切换外观
已实现:桌面壳层的第五个变体 E,index.html?variant=duo;第 1–4 节是实现前的记录,与它的出入见第 5 节,验证见第 6 节。2026-10-29 展开 / 合上这一拍照 iPhone Duo 的真实开合重做(分支 codex/duo-unfold):1.2、3.2、3.3、3.6、5、6 节已按新做法改写,答一件、落地、桌面、详情与手势不变。同日晚上用户说“折叠屏需要 iPhone Duo 的那个手机壳呀”,又给它装上机身(分支 codex/duo-body):1.2 末尾加了机身的观察与出处,新增 3.7,3.6、5、6 节相应改过;信息层、开合的时间与阈值都不变。2026-10-29 夜,用户看过装上机身的版本说圆角对不上、没太体现双屏的优势,左页改成“摊开被折掉的原料”,屏的圆角改成与机身同心(分支 codex/duo-unfolded):新增 1.6 记反馈、方向与圆角的量法,2.1、2.2、3.1–3.6、3.7 的圆角一行、5、6 节按新做法改写;翻页、毛玻璃、机身、开合的时间与阈值都不变。同夜用户又说展开界面的上下滚动逻辑不对、页头应该留着(分支 codex/duo-scroll):新增 1.7 记原话与手机 app 的常识,3.2、3.3、3.5 里涉及滚动位置的地方、4.1、4.2 改写,新增 4.5 钉住区与滚动区(连同按用户操作顺序走一遍修掉的地方),5、6 节相应改过;翻页、毛玻璃、机身、圆角、开合的时间与阈值都不变。2026-10-30 用户说桌面模式适配很差、让选一两个选项之后截图看看:答完一件,那一行 / 那一列留着原来的尺寸,回执贴在一角,周围一大片空,书本也是(分支 codex/duo-answered):新增 1.8 记原话与截图里看到的,3.2 第 2 拍、3.4、4.1(桌面的列宽固定、两半一起横滚、答过的列挪到最后)、4.2、4.5、5、6 节改写;翻页、毛玻璃、机身、圆角、钉住区与滚动区、开合的时间与阈值都不变。
Duo 折叠屏 · 拆开的卡:参考、数据、故事
状态:2026-10-28,第三轮六步流程(reference → data → story → motion → direct → ship)走完,分支 codex/motion-duo。前三步加交互表与静态草图先成文(第 1–4 节、第 7 节),四个待定点由用户定下(4.4)后实现。原型探索,不是规范。起点是 D 手机首页(下称 D,现状与交互清单见 home.md);两层法则与交互标准见 CONCEPTS.md,交互共用 motion/ix.mjs。数字都由 scenario.mjs 回放现算,口径与 variants/d.mjs 相同(2.1);推算脚本只留在本机,不入库。
一句话:合上是折好的,翻开是摊开。 合上是 D,整理好的结果;展开时左页把这一轮进来的输入按时间摊开(f26 是 158 条):采用的按落进哪件分组,和右页那张卡同一行、同一高,一条线从组里穿过折痕汇进那张卡;进了 Agent 那边的、暂缓、噪音、重复,和这一轮撤下或合并的,灰着折在左页下面。右页就是外屏,合上时留在手里。英雄数字从“5 件”翻成“38 份等你回应”,64 格份额带的分界线压在折痕上。它不是放大的 D:左页上的东西,D 里一样都看不到。(2026-10-29 夜之前这里写“铰链是 Agent 与你的分界”,左半是 Agent 为这件事做了什么;为什么改见 1.6。)
1 参考(reference)
1.1 iPhone Duo 的几何
| 项 | 值 | 出处 |
|---|---|---|
| 内屏 | 7.6 英寸(7.58),1878 × 2670 px,430 ppi,ProMotion 120 Hz | 规格页 |
| 外屏 | 5.4 英寸(5.36),1398 × 2034 px,460 ppi,ProMotion 120 Hz | 同上 |
| 宽高比 | 发布会说两块屏 “consistent aspect ratio”(keynote 55:52);The Verge 写 1.4 : 1。由像素算:内 1.422、外 1.455,都约等于 √2 | keynote;The Verge 评测 |
| 方向 | 合上时外屏竖放;像书一样展开时内屏横放。两块屏高度几乎相等(112.3 / 110.9 mm),内屏宽约为外屏两倍,所以内屏的一页约等于一块外屏 | 规格页尺寸推算 |
| 点尺寸 | 未公布。 假设外屏为 @3x 原生:466 × 678 pt。内屏按 App Store 截图规格 2853 × 2007 px 算是 951 × 669 pt(@3x 渲染后缩到面板,缩放约 1.069,与 6 Plus 当年的做法同类),一页 475.5 pt 宽。按面板直接除 3 是 890 × 626 pt。本文取 951 × 669 | 推算,未证实 |
| 机身 | 展开 164.6 × 117.8 × 5.2 mm,合上 84.1 × 117.8 × 11.3 mm,254 g;1999 美元,10/16 预购,10/23 发售 | 规格页;Newsroom |
| 铰链与折痕 | 铰链居中,变阻尼,可以停在大多数角度;折痕“摸得到多于看得到” | The Verge;Engadget 评测 |
| 外屏 | 铰链一侧(左)圆角小,外侧(右)圆角大;灵动岛在右上;状态栏、工具栏、标签栏竖排在右缘 | HIG · Designing for iPhone Duo |
| 内屏 | 屏下摄像头在右页上沿,约屏宽 69–70% 处;折叠区是中线上一条竖带,从 HIG 图上量约占屏宽 2.8%,约 27 pt(量测,未证实) | HIG 配图 |
| 姿态 | 合上、帐篷、全开横(书本)、半折、全开竖、笔记本;API 只报三种状态:closed / partiallyOpen / fullyOpen | HIG |
| 尺寸类 | 内屏 regular / regular;外屏竖放 compact / regular | HIG |
| 新 API | onHingeChange / UIHingeInteraction:报铰链角度,精度约 5°,Apple 的说法是“适合驱动效果,布局请用排布与区域 API”。ReservedRegion:.division 指半折时的折痕区,.occlusion 指遮挡区。ArrangementView:.split / .overlay | Tech Talks 111461–111466 |
| 兼容 | 没用 iOS 27 SDK 构建的 App 在内屏上带黑边;用 iOS 27.1 SDK 构建的铺满 | HIG |
| 分屏 | 两个 App 各占 50%,各自的控件贴在自己那一侧的外缘 | HIG;Newsroom 配图 |
未证实,本文按假设往下做。
- 点尺寸与缩放倍数,即上表的推算。
- 内屏从多大角度开始接管、多大角度报告 partiallyOpen / fullyOpen,Apple 没有公布。草图假设内屏约 20° 接管,约 120° 报告稳定展开。
- 展开途中,第三方 App 能否逐帧拿到铰链角度、自己绘制内屏,还是由系统用快照过渡。Tech Talk 只说
UIHingeInteraction适合驱动效果,没有讲展开过程的帧级行为。本文假设 App 自己画。 - HIG 的内屏主屏图把外屏第 1 页放在左页,发布会演示却把它留在右页贴右缘。本文跟发布会:右页 = 外屏。Tech Talk 111463 的说法与此一致:半折时提示框挪到 trailing 一侧,因为那里更靠近“合上后它会出现的位置”。
1.2 系统怎么展开
- 发布会主屏演示(keynote 57:48–57:51,
duo-anim-apple-keynote-t3468.0…3470.6,约 2.2 秒,跟手)。外屏内容先轻微发糊(t3468.0–3468.4)。前板绕左侧书脊向左翻开,翻开的那片像一扇半透明的毛玻璃窗(t3468.8–3469.6)。内屏右页是同一个主屏、同样大小、贴着右缘,Dock 竖排在右缘不动,状态栏也不动。左边新出现的区域(“On This Day”小组件)先暗、先糊,到 t3470.6 才清楚。Federighi 的原话是“the screen has become transparent… a window to what is underneath it”。 - Tech Talk 111464 锁屏(《Leverage multiple displays and scenes on iPhone Duo》,49.5–51.5 秒展开、53.5–54.5 秒合上,2026-10-29 按 30 fps 逐帧看过;早先截的
t50.7…54.4是其中一段)。镜头从铰链一侧看,两块板对称张开。右半一开始就是清的;左半先黑,壁纸从折痕往外“渗”出来,由糊变清;转到一半时板的自由边迎着镜头、显得更高;跨折痕的时钟最后从右页中心滑到整个跨页的中心(讲者的说法是“wallpaper reacts to the angle of the hinge and applies a zoom effect”)。合上是倒放:暗与糊从外缘扫向折痕。 - empirical.health 的逆向。靠铰链处清楚,越往活动边越糊,模糊随离铰链的距离按三次方增长:30° 时自由边约 48 px,到铰链一半处只有 6 px;内屏起初更暗、随开合变亮,活动板靠自由边更暗;两块屏采样同一张虚拟画面平面,所以画面不跳。它把这概括为“content pinned in space”:内容钉在空间里不动,展开只是露出更多;它自己的原型也是细节页留在右边,列表从左边长出来。
- Apple 没有做“从中线往两边推开”。 那是 AOSP 的
UnfoldMoveFromCenterAnimator:左右两半各从中线外移 8%(TRANSLATION_PERCENTAGE 0.08),进度 = 角度 / 165°,弹簧刚度 600,固定时长后备 400 ms,有一层亮光揭开,进度到 0.8 之前屏蔽触摸。
四条精髓(2026-10-29 对照上面三处定下,按重要性):
- 物理翻页。 右页(外屏那一页)一像素不动、始终清楚;活动的那一页绕铰链轴真的转,带透视:转到一半时自由边迎面、显得更高更大,然后落平。合上是倒放。全程由铰链角度驱动,手停在半路,画面就停在那一帧。出处:Tech Talk 49.5–51.5 秒;keynote t3468.8–3469.6。
- 磨砂玻璃。 活动页上的内容像隔着一块绕一边铰接的毛玻璃看:靠折痕清楚,越往自由边越糊、越暗;角度越开越清、越亮,全开时全清。合上时暗与糊从自由边扫向折痕。模糊是空间渐变(离折痕的距离)× 角度,不是整页一个值。出处:Tech Talk 49.5–51.5 秒与 53.5–54.5 秒;keynote t3468.0–3470.6;empirical.health 的三次方与暗度。
- 内容钉在空间里。 展开只是露出更多:内屏右页与外屏同内容、同位置;左页是最终跨页的左页,钉在终点,被活动页逐渐“揭”出来。出处:keynote(“a window to what is underneath it”);empirical.health。
- 画面重新取景。 跨折痕的东西随铰链重新取景:锁屏的时钟从右页中心滑到跨页中心。出处:Tech Talk 49.5–51.5 秒的后段。
从中读出的节奏。 一,手里那一半先稳住:原尺寸、原位置,一像素都不动。二,新的一半从折痕往外露出,先暗、先糊,后清;它不是平着擦出来的,是一块板翻过来。三,跨过折痕的东西最后才重排。四,合上是倒放。时长两档:跟手的部分随手走(演示里约 2.2 秒,Tech Talk 约 2 秒);系统自己补完的部分不超过 0.7 秒。
机身(2026-10-29 晚量的,3.7 照它画)。
- 出处。 Apple Newsroom《Apple unveils iPhone Duo》媒体包里的 3840 px 正面渲染:
Apple-iPhone-Duo-Netflix-app-260909.jpg(平放,主要量这张)、-colors-(四色,合上的正面、背面与书脊)、-Detail-app-、-display-sizes-、-Slack-app-、-Zoom-app-;Tech Talk 111464 的 49.3–51.7 秒按 30 fps 抽的帧(侧面与翻页途中)。 - 平放(Netflix-app)。机身 x 395–3435、y 196–2372 px,按规格的 164.6 × 117.8 mm 是 18.47 px/mm,长宽都对得上。四边一样宽,约 62 px(3.36 mm),由外往里:抛光金属边、一道亮的唇、炭灰屏框(#313234)、像素前一圈纯黑。机身圆角约 228 px(12.3 mm),屏的圆角约 54 pt。
- 键、铰链与天线断点。 右半上沿两颗键(2605–2805、2850–3050 px),凸出 9 px(约 0.5 mm);右缘一颗长键(高度的 0.28–0.44),其下一颗平齐的键(0.67–0.78)。折痕上下两端,金属边各往里收一个缺口,中间是一块约 67 pt 宽的铰链盖(里面一块 56 pt 的面板)。金属边上的天线断点在离折痕约 ±93 pt 处与近四角处,两侧的在上下各一道。
- 合上(colors)。铰链一侧圆角小(约 19–24 pt),外侧大(约 77 pt),四边约 2.7–2.9 mm;前盖是黑玻璃屏框,书脊是拉丝金属。相机凸台在右半背面,正对屏幕的镜头里看不到。
- 翻页途中(Tech Talk)。板的自由边是一条抛光金属,侧立、迎着镜头时最宽,一道亮线顺着它走;越平越窄,平放时只剩一线。合上时从侧面看,是两块各带金属边的板叠在一起。
- 许可:Apple 的图只作参考。 Newsroom 媒体包附的 LEGAL_NOTICE 只许个人或新闻报道、非商业使用,而且不得改动。Apple Design Resources 的 iPhone Duo 机身包(
Bezel-iPhone-Duo.dmg)要先点同意,许可只许拿来做 UI 样稿,不许提取、改动和再分发;没有同意,下载的文件已删。Framer 上的“iPhone Duo Mockup”售 29 美元、没写许可;Mockey、Screenhance 的条款不清或带水印。这几样都没用。所以仓库里没有 Apple 的一个像素:只取量出来的尺寸与颜色,机身全是自己写的 CSS 盒子与一个内联 SVG(铰链处的缺口)。渲染图量完只留在本机。
1.3 Apple 对第三方的要求
- “Keep functionality and the state of elements the same between displays.”“Avoid extreme layout changes… favor small adjustments over rearrangement.” 控件留在原来那一侧,“to preserve a continuous experience at the same vertical height”。
- 半折时,内容从折痕两侧让开(keynote 1:03:44);提示框挪到 trailing 一侧(Tech Talk 111463)。
- 分屏时各自的控件贴在外缘;折叠区里不放可点的东西(三星、微软的规范也这样写)。
1.4 别家的范式
| 来源 | 做法 | 搬什么 | 不搬什么 |
|---|---|---|---|
| Microsoft 双屏 | 扩展画布、列表—详情(预选一项)、双页、双视图(两屏同步选中)、伴随窗格 | 伴随窗格:左页是右页的伴随,不是另一份内容;双视图的跨屏同步选中 | 列表—详情一次只显示一件的详情 |
| Android 规范布局与姿态 | 列表—详情、辅助窗格、信息流;桌面与书本姿态;FoldingFeature;Tier 3 连续性(滚动位置、文字、播放不丢) | 辅助窗格;连续性:展开、合上都不丢滚动位置和作答状态 | — |
| 三星 Flex Mode | 半折时内容在上、控件在下;姿态切换可见;折痕附近不放可点的东西 | 桌面姿态“上看下按”;折痕两侧留白 | — |
| Fantastical / Readdle Calendars(Duo 版) | 左页概览(月份 + 今天),右页细节(周);同一天在两页同时高亮 | 跨折痕联动高亮:长按色带上的格子,对应那条描边 | 概览—细节是两套内容;这里每一行是同一件事的两半 |
| empirical.health | 内容钉在原处,展开只是给左边加上下文 | 钉住;展开加的是上下文,不是放大 | — |
| Pixel 桌面姿态 YouTube | 视频在上,控件在下 | 同三星 | — |
| ManyFold | 折叠角度驱动语义缩放 | — | 展开不是放大:结构不变,才认得出是同一张卡 |
1.5 看过的图
全部在 .local/inspiration-duo/(不入库),共 55 张,草图对照页收了其中 6 张(第 7 节)。
| 图 | 看到什么 |
|---|---|
duo-01 / duo-02 HIG 外屏、内屏主屏 | 外屏竖排的 Dock 在右缘;内屏是两页主屏,图标网格不跨折痕 |
duo-03 六种姿态 | 帐篷用的是外屏横放(摄像头转到右下);“全开竖”是铰链横着的内屏 |
duo-04 外屏竖排系统条 | 灵动岛、9:41、Wi-Fi、返回与更多、标签栏,从上到下排在右缘一条约 56 pt 宽的带里;左边整片都留给内容 |
duo-05 内屏区域 | 折叠区是一条浅蓝竖带,约占屏宽 2.8%;摄像头的避让区在右页上沿 |
duo-06 备忘录半折 | 文字从折痕两侧让开,左右各成一栏,不跨折痕 |
duo-07 分屏控件 | 每个 App 的工具栏贴在各自的外缘 |
duo-anim-apple-keynote-t3469.2 | 前板翻开到一半,半透明;右页主屏已是最终大小和位置 |
duo-anim-apple-techtalk111464-t51.1 | 半开时右半清楚,左半糊;时钟正好压在折痕上 |
pat-07 Android 邮件折展 | 列表—详情,展开时列表从左边挤出来,详情跳位,不连续 |
pat-08 三星 Flex 视频 | 视频上移到上半,控件落到下半,过渡可见 |
pat-13 empirical.health 展开 | 细节页不动,左边新出的一条先暗着,再拉开 |
pat-14 Fantastical | 左页深色概览,右页浅色周视图;FRI 23 在两页同时高亮;右缘竖排时间、Wi-Fi、搜索与加号 |
pat-16 ManyFold | 角度越大层级越深,属于放大类 |
1.6 第二轮反馈(2026-10-29 夜)
用户看过装上机身的版本(#43)后说了两句:
圆角不太对的上啊。
流畅挺流畅的,但是没太体现双屏的优势。
圆角。 那一版机身是 74 pt 的角,屏却取了 36(为了让系统条的时间不被弧切掉),外屏铰链一侧取 10:屏与机身不同心,边框到了角上忽宽忽窄,一眼就对不上。重新量过(渲染图只留在本机):
- Netflix-app(平放):机身的角约 230 px,边框约 62 px,屏的角约 167 px,230 − 62 ≈ 167,同心;换到屏的比例约 75 − 20 ≈ 55 pt。
- display-sizes(合上,正对):外侧两个角同心;铰链一侧很紧,屏的角约 4 pt。colors 上合上的机身铰链一侧约 19–24 pt。
- Tech Talk 111464 里那张“合上”的帧,看到的是外侧(侧键那一边),不是铰链。铰链脊是圆的,说的是它的横截面:从侧面看,两块板合起来的那一边是一条圆脊;正对屏幕看,它是一条直边带两个小角。
- 真机上的时间离右缘约 33 pt、离上沿约 35 pt:系统条本来就在安全区里,不贴着角。
于是改成:每块屏的每个角,都是机身那个角的半径减去两边的边框(上下与左右的边框不等时是椭圆角)。内屏上 54 / 56、下 54 / 51;外屏外侧 54 / 56、54 / 60,铰链一侧 6.5 / 6、6.5 / 10(机身那一侧仍是 24,照 colors 与 display-sizes);桌面是书本那一副转过来。内屏在折痕处没有角,左页、右页靠折痕的两角是直的。系统条不再靠缩小圆角来让位,而是整条移进安全区:内屏右页在外屏灵动岛的位置留一块同样大的空位,时间在两块屏上都离上沿 52 pt(上边距 16 + 灵动岛 26 + 间隔 10),离弧足够远;展开时它不动,3.6 的“系统条不动”照旧成立(之前外屏的时间在 50 pt、内屏的在 14 pt,靠内屏缩到 36 的圆角让开)。数在 duo-motion.mjs 的 BODY.corners,由 BODY 现算。
双屏的优势。 那一版的左半写 Agent 为这件事做了什么:标题、档位、变化、摘要、为什么升档、本轮几条。这些在 D 的卡上和详情里都有,展开只是把它们挪到左边,多出来的那一页没有新东西。用户选的方向是摊开被折掉的原料:合上时的外屏是折好的结果(D);翻开时,多出来的那一页把它是怎么折出来的摊开:这一轮进来的每条输入,哪几条成了右页的哪张卡,哪些进了 Agent 那边,哪些没进任何一件。这是一块屏放不下的:D 只能把一件事的来源收在“详情 ›”里,一次看一件;摊开以后,一眼看见右页每张卡背后是哪几条原话。规则见 2.1,f26 的样子见 2.2,各拍见 3.3–3.5。
1.7 第三轮反馈:滚动(2026-10-29 深夜)
用户看过摊开的版本(#44)后说:
展开界面的上下滚动逻辑不太对吧。第一,应该很多区域不能滚动吧,比如头部明显要保留,下面部分页内滚动更合理。你看下用户的操作逻辑。
那一版的内屏是一整张跨页放在一个滚动容器里:两页页头(日期、“158 条输入 / 38 份等你回应”、158 格细格、去向那一行)、份额带、折痕两侧的账,都跟着条目一起滚走;外屏用 D 的整页滚动,页头也滚走;桌面姿态干脆不滚,下半放不下的卡被裁掉。手机 app 的常识是另一样:
- 页头、工具条、标签栏是壳,钉住;只有内容在它下面滚(邮件、信息、日历的列表都这样;iOS 的大标题滚动时收成小标题,但停在顶上)。
- 一块屏上可以有几个滚动区,各自滚(iPad 的分栏、Android 规范布局的列表—详情);但一件事的两半要同行,就只能一起滚(4.2 细则)。
- 换姿态不丢位置:Android 的 Tier 3 连续性要求折叠、展开后回到同一处内容、同一个滚动位置(1.4);iOS 改变场景尺寸时也保留滚动位置。
- 一个手势只滚一个列表:滚到头不把整页拖着走,从页头上开始的拖动也不交给页面。
于是:每块屏的页头钉住、条目在下面滚(4.5);外屏也钉住页头,好让“右页 = 外屏”在滚过之后仍成立(3.3);再按一个真用手机的人的顺序把操作整个走一遍,修掉不像 app 的地方(4.5 末的表)。
1.8 第四轮反馈:答完一件(2026-10-30)
用户在桌面姿态里点过一两个选项以后说:
桌面模式,似乎适配很差。你选中一两个选项之后,截图看看。
截出来(1440 × 900 与 1180 × 760;composite f26、f22,story f04;截图只在本机 scratchpad),桌面和书本是同一个病:答完的那一件留着原来的尺寸,回执只占它的一角。
- 桌面,列。 列宽按列数平分(
minmax(0, 1.25fr) minmax(0, 1fr) …)。答完一件,compose 照 D 把下一件从“还有 N 件”里提上来,三列变四列,四列挤在 585 pt 里,一列约 131 pt(第一列 163)。答过的那一列里,回执的字夹在勾与“撤回”之间只剩约 50 pt 宽,“你选了「整根挖开换」· 00:00 已保存……”一个字一行,“撤回”压在字上;别的卡标题一字一行(“#12088、#13295、#13350 三个旧 PR”竖着排),按钮挤扁。窗口小(1180 × 760)时整台缩到 0.63,没答也是一列 176 pt,屏上约 110 px。 - 桌面,上下。 上半答过那件的组折成一小块贴在列底,上面一大片空;没答的从顶上排。下半的回执贴在卡底(卡被同一行别的卡撑高)。两半只剩“同一列”对得上,看上去对不齐。
- 桌面,页头。 副标题“4 件 · 新问题 2 个 · 你在原处处理了 2 件 · 你在这里已回应 1 件”折成两行,把份额带往下推,整个下面跟着挪。
- 书本。 上一版为“两半同行”定了“作答后卡高不变”(3.4 的旧版),答完的右半保持原高,回执贴在卡底,左页那一组折成一行贴在行底,两半上面都空着;答两件就是两个大空壳。
D 早有答案:已答卡是标题一行加回执的一张矮卡,下面的卡一次上移补位,折叠里的一件补上来(home-motion.md 的 3a)。于是:**答一件 = 这一件的那一行(桌面上那一列)整体收成回执的尺寸,其余的补上来;两半仍然对齐,对齐的是收起之后的高度与列。**书本见 3.4;桌面的列宽固定、两半一起横滚、答过的列挪到最后,见 4.1。
2 数据(data)
2.1 规则
| 量 | 怎么算 |
|---|---|
| 条的集合与顺序 | 与 D 的 compose() 相同:开着的问题按 urgencyKey 排序(档位 → 24 小时内到期 → 到期 → 份额倒序 → 名次);前 3 件是大条,其余在合上时折成“还有 N 件”,展开后成单行条 |
| 本轮的输入 | frame.events,按时刻排。每条写时刻、来源(channel 的第一段)、谁(from;你自己发的写“你”,与来源同名的不写)、一行主题(subject,没有就用 text) |
| 落到哪件 | 与 C 流动形态同一条规则,抽成 variants/inflow.mjs(从 c.mjs 原样搬出,C 的画面逐字节不变):先看这一批声明引用了哪条输入(batch.declarations[].sources 的 event:),再看输入自己的 relatedIds[0];沿合并走到并入的那件,沿父级走到根层。只有采用(used)的落进一件;暂缓、噪音、重复、未判断的不落 |
| 左页的组 | 落进右页某张卡的输入,是那张卡的一组,排在那张卡的同一行:同一行、同一高(网格行拉齐)。同一来源、同一线程(主题去掉 Re: 一类前缀)连着的几条并成一行,写 ×n 与几个人;大条首条最多 6 行、其余 4 行,多出的并成“还有 N 条 · 来源 计数”;单行条的左半一行:本轮几条、最后一条的来源与人。组的标题行是事项名、本轮变化、档位与份额,第二行“本轮进了 N 条”与两周份额走势(itemFacts().trend) |
| 线 | 每个进了输入的组有一条线:从每行的末端汇成一股,穿过折痕,接进卡的左缘(卡上一个小圈)。没进输入的组没有线;你答过的那张卡,组折成一行,也没有线 |
| 灰的 | 落进 Agent 那边某件(它不在右页)的,一件一行(件名、×n、来源),排在左页“Agent 在办”之上;不落进任何一件的,按处置与来源各一行(每种处置最多三处,超过四处时其余并成“其他 N 处”),写 ×n、时段与第一条的主题,连同这一轮撤下或合并的几件(上一轮在页上、这一轮不在的,写撤下或并入哪件),排在左页最下面“折掉的” |
| 左页页头 | 本轮的时间窗(signal.window)、“N 条输入 · 累计 M 条”、一排细格(每条输入一格,按时刻落在时间窗上:高而黑的进了右页,中而灰的进了 Agent 那边,矮而浅的折掉)、“a 条成了右页的 k 件 · b 条进了 Agent 那边 · c 条折掉”。a + b + c = N |
| 份额带下的账 | 以折痕为界:左“空余 1 + 25 份在 Agent 那边(9 件)”,右“+ 38 份等你 = 64 份”;答过还没被读到的另写“+ N 份你答过”。随份额带一起长出来 |
| “本轮进了 N 条” | 概要卡与组的第二行都按上面的规则数(itemFacts(i, id, intoRoot)),组里有几行原文,加起来就是几条 |
| 右半(你这边) | 到期(ask.due_at,没有就取 item.due_at)、“新问题 / 问题已更新”、ask.prompt、你在原处做过什么(feedbackTrail)、作答控件(respond.mjs 的 responseHTML),“稍后 / 评论”放在第一行末尾 |
| 英雄数字(份) | you = 开着且你还没答的问题的份额之和 |
| 你答过、Agent 还没读到 | pending = 这一页已答的问题的份额之和(只在本页模拟,不写回回放) |
| Agent 那边 | agent = root.used − you − pending,也就是 D 的“进行中与结果”加次要事项 |
| 空余 | blank = 64 − root.used |
| 上次以来那一行 | 只留在右页:新问题几个、你在原处处理了几件;收到几条、怎么分的,由左页页头写(2026-10-29 夜之前它拆成两半,左页一半写“之后收到几条、几处变化”) |
2.2 f26 展开时(composite 默认帧)
8/16 00:00「车道下的铜管裂了」。本轮窗口 8/15周六 12:00 → 00:00,158 条输入(采用 40 · 暂缓 84 · 噪音 31 · 重复 3),累计 3051 条;用 63 份,空余 1 份。
- 右页:38 份等你回应。 5 件 · 新问题 2 个 · 你在原处处理了 2 件。
- 左页页头:158 条输入 · 累计 3051 条。 158 格细格;“34 条成了右页的 3 件 · 6 条进了 Agent 那边 · 118 条折掉”,34 + 6 + 118 = 158。
- 份额带下的账: 空余 1 + 25 份在 Agent 那边(9 件)|+ 38 份等你 = 64 份。
| 条 | 左页的组(与右页那张卡同一行) | 右半 |
|---|---|---|
| 1 主水管 | 车道下铜管裂,周一前定修法 · 变大 12 → 24 · 档位 关键 · 24 份;本轮进了 16 条,并成 5 行:12:40 SMS Ronny, Rörfixarna(报价那条);13:10 iMessage Kajsa、你 ×6;13:48 问答 你、Linus 等 4 人 ×4;14:30 Trygghem Försäkring(理赔受理);16:29 问答 Wilma、Bertil Brandt 等 3 人 ×4。五股汇成一条线进卡 | 10:00 前 · 还有 34 小时 · 新问题;“车道下的主水管:整根挖开换(约 6.8 万克朗),还是免挖穿新管(约 4.1 万)?”;13:40 你用 iMessage 回复 Kajsa;整根挖开换 / 免挖穿新管 |
| 2 FTPS | #13587重开,修复#13596待你审 · 变大 1 → 6 · 6 份;本轮 12 条,4 行,都是 GitHub:issue #13587 的回复 ×7、PR #13596 ×2、issue 的回复 ×2、Copilot 的评审 | 新问题;批准(推荐)/ 要求修改,评论 |
| 3 待审 PR | 3个旧PR待审,15日合#13591 · 6 份;本轮 6 条,1 行:12:01 GitHub,PR #13591 的评审 ×6 | 已等 36 小时;推到…;22:35 你在 GitHub 上合并了 #13591 |
| 单行 | Signe 19日开学 · 1 份;Ture 已退烧 · 1 份;本轮都没有新输入,没有线 | 同意书签了吗? · 8/18周二前 · 已签;VAB 报了吗? · 已申报 |
左页下方,先是进了 Agent 那边的 6 条:master仍红 ×3(CI、GitHub)、Kajsa 提醒修旧车刹车 ×1、Mamma 选疏散公寓 ×1、Kräftskiva 报名(已撤下)×1;然后 Agent 在办 · 2(master CI 6 份、Smilla 6 份),另有 7 项次要事项(13 份);最下面“折掉的 118 条”:暂缓 GitHub ×31、问答 ×27、论坛 ×22、其他 3 处 ×4,噪音 论坛 ×27、订阅邮件 ×2、WhatsApp ×1、Mastodon ×1,重复 WhatsApp ×2、GitHub ×1。右页的末行说“等你的就这 5 件。左页下面是 Agent 那边的 9 件和折掉的 118 条,不用你动。”38 + 25 + 1 = 64。
2.3 一次真实作答:f22 → 打给诊所 → f23
| 时刻 | 右页(份等你回应) | 左页(份在 Agent 那边) | 空余 | 发生什么 |
|---|---|---|---|---|
| f22 8/14 00:00 | 58(5 件) | 6(6 件次要) | 0 | Smilla 24 份(08:00 前 · 还有 8 小时)、Mamma 24、#13572 6、VAB 3、同意书 1 |
| 按“打给诊所” | 34 | 6 | 0 | Smilla 的右半就地成回执;另 24 份你答过,等 Agent 读到 |
| f23 8/14 12:00 | 10(3 件) | 29(8 件) | 25 | Smilla 带着回执过折痕,成 Agent 在办第 1 行(24 → 12);Mamma 随改写失效(24 → 6);#13572 已批准,剩下三个旧 PR 成新问题(6);VAB 3、同意书 1 上移 |
D 上同一段是 5 → 4 → 3 件。这里是 58 → 34 → 10 份:答掉的那件,一件就是 24 份。
2.4 story 数据集
同一套规则也能跑,草图用 ?data=story 打开。story 的 f04 → f05:件数 4 → 3,份额却 33 → 39。答了一件,等你的反而更重,因为让出的份额被一个新问题接走了。D 的“N 件”在这里只会往下走一格;“N 份”会往上翻,这正是换成份额的理由。story 的 f26 是 1 件 / 1 份。
2.5 两周的范围
| 数据集 | 展开时“份等你回应” | 中位 |
|---|---|---|
| composite | 10–58 | 38.5 |
| story | 1–48 | 24 |
两位数之内,一帧翻完,不需要滚动计数。
3 故事(story)
论点。 合上是折好的,翻开是摊开。外屏是这一轮整理好的结果;翻开时,左页把它是怎么折出来的摊开:这一轮的输入按时间排,进了右页哪张卡的就是那张卡的一组,与卡同一行、同一高,一条线从组里穿过折痕接进卡;没进右页的灰着折在下面。右页就是外屏,留在你手里。答一件,右半原地收成回执,左页那一组也折起来、线缩回折痕:原料已经变成你的回答。下一轮整理落地,回执过折痕,这件事到了 Agent 那边,左页换成下一轮的原料。
英雄数字:“N 份等你回应”。 与 D 的“N 件等你回应”是同一组问题,换成份额来量。外屏上仍是“5 件”:外屏小,一瞥只需要“几件”。展开以后,64 格份额带一页宽、分界线压在折痕上,带下一行账写着 Agent 那边的 25 份,这时该回答的是“64 份里有多少压在我这边”。于是数字在整理段 0.22 秒那一帧,在同一位置从“5 件”翻成“38 份”;合上时翻回去。账上的两个数加上空余正好是 64(f26:38 + 25 + 1)。为什么不重复 D 的“件”:件数会骗人。f26 的 5 件里主水管一件就占 24 份;story 的 f05 少了一件,份额却多了 6。
样张。 第 1 拍用 composite f26,最满的一页:5 件、三种作答形式。第 2、3 拍用 f22 → 打给诊所 → f23,两周里最大的一次让位(2.3)。story 数据集对应 f04 → f05(2.4)。
3.1 左右分什么
用户最初的方向是:展开之后,重要的事被分解成一种左右分配的效果。下面几种分法的判据有两条:两边都得是用户真会看的东西;合上时两半要能合回一张卡。2026-10-29 夜加了第三条:左页上得有一块屏放不下的东西(1.6),于是从 A 换成 E。
| 方案 | 左 | 右 | 得 | 失 |
|---|---|---|---|---|
| E(2026-10-29 夜选)摊开的原料 | 这一轮的输入,按落进哪件分组,与卡同一行;一条线穿过折痕接进卡;没进右页的灰着折在下面 | 要你答的:期限、问题、你在原处做过什么、作答 | 左页是 D 里看不到的东西:右页每张卡背后是哪几条原话、谁说的、什么时候;没进来的也在,是灰的。合上时外屏是折好的结果,翻开时是把它摊开。卡还是一行两半,合得回去 | 原话长,一组最多 6 行,其余并成“还有 N 条”;158 条里 118 条只能灰着并成二十来行 |
| A(第一版选) | Agent 为它做了什么:来源、本轮进了几条、走势、为什么升档、样例 | 要你答的:期限、问题、你在原处做过什么、作答 | 两边都是作答时真要看的:右边决定怎么答,左边决定信不信。D 把左边那些藏在“详情 ›”后面,书本上把它摊开在同一行。合上时左半收回成卡上的标题与摘要行,右半本来就是那张卡,能合回去 | 左半比右半字多,书本一页只放得下 3 条大条加 1 条单行;其余靠滚动。左半写的在 D 的卡上与详情里本来就有,展开只是挪了个地方(1.6) |
| B | 等你的 | 进行中 / 已答 | 分区清楚 | 拆的是区,不是卡,实质是 D 排成两栏;f26 的右栏只有 2 行加次要事项,空;合上时是两个列表首尾相接,不是一张卡合回去 |
| C | 上一轮 | 这一轮 | 换轮的差别一目了然 | f26 的前两条都是新问题,左边是空的;差别只在换轮那一刻值得看,平时左页空置 |
| D 列表—详情(微软、Android 规范布局) | 全部事项的列表 | 选中那一件的详情 | 成熟范式 | 一次只看一件的详情;拆分不在每张卡上,看不出“每件事都有两面” |
选 E。 它与 A 共用一个几何事实:64 格份额带一页宽,你的格子(墨色)在折痕右边,Agent 的格子(灰)和空余在左边,分界线压在折痕上。答一件,墨格就地变空心;整理落地,空心格跨过折痕变灰。“轮到谁”于是成了一条线的位置。E 又多一种线:输入进卡的那一条,答完就缩回折痕;“从哪来”与“轮到谁”都画在折痕上。
3.2 三拍
| 拍 | 时长 | 发生什么 | 观众此刻 |
|---|---|---|---|
| 1 展开 | 跟手(翻页),120° 时加 0.40 秒(整理) | 右页不动,就是外屏;承载外屏的那块板绕折痕向左、朝你翻开,正面由清变成毛玻璃、渐渐透明,背面就是左页(摊开的原料),靠折痕先清、越往外越糊越暗,落平时全清;120° 时右半压紧,折起的几件展开成单行条,“5 件”翻成“38 份”;份额带与组进卡的线随铰链从折痕长出(120° → 170°);两页页头、份额带与账钉在上面,只有条目在下面滚,外屏上正看着的那张卡在右页上接着读(3.3) | 右页每张卡背后是这一轮的哪几条原话;38 份在我这边 |
| 2 答一件 | 0.72 秒(桌面 0.92) | 那一行整体收成 D 的已答卡:右半成标题一行加回执,左页那一组同一拍折成一行、与它同高,进卡的线 0.18 秒缩回折痕;0.20 起下面的行一次上移补位,折叠里的下一件长成一行;38 → 14 份;那 24 格就地变空心(2026-10-30 之前卡高不变、不补位,见 1.8) | 原料变成了我的回答,等 Agent 读 |
| 3 整理落地 | 1.0 秒 | 回执填实,一次斜穿折痕,和左半一起落到 Agent 在办;空心格过折痕变灰;下面的条上移;左页换成下一轮的原料(同一套移位与进出场,不飞点) | 这件到了 Agent 那边,我这边剩 10 份 |
3.3 第 1 拍:展开(合 → 开)
连续性。 右页 = 外屏,一一对应:合上时最后一帧的每张卡,在内屏右页上 x 距右缘不变、y 不变、滚动位置不变,系统条(时间、记录)在右缘不动,只是灵动岛换成了屏下摄像头。所以翻页阶段,你手里的那一半没有任何东西移动。左页是新出来的:摊开的原料画在翻开的那块板的背面,就在终点,每一组和它那张卡同一行、同一高度,板翻开时被“揭”出来;身份靠位置,而不是靠飞行动画。组与卡之间的线不在画上的终点状态里,它跨着折痕,随铰链从折痕长出来(下表)。
滚动位置(2026-10-29 深夜改)。 两块屏都把页头钉住、条目在下面滚(4.5),所以“y 不变”说的是条目区里的位置。外屏的页头高 118 pt,条目从 132 pt 起;内屏右页的页头连同份额带与账高 161 pt,条目从 175 pt 起,两者差 Δ = 43 pt。翻页阶段右页就是外屏本身,什么也不动;书本的滚动这样接上:
- 翻开。 取外屏上你正看着的那张卡(页头下面露出至少 24 pt 的第一张、两块屏上都有的),书本滚到让它在右页上的 y = 外屏上的 y + max(0, Δ − 外屏滚过的距离)。外屏在顶上时书本也在顶上:左页从顶上读起,这一条照旧;外屏滚过 43 pt 以上,那张卡在两块屏上同一高度,接着往下读;滚得不到 43 pt 时按差额过渡,不会有外屏露着的东西藏到内屏的页头底下。书本滚不到的地方(列表到底了)截住。
- 合上。 倒过来:外屏滚到让书本上你正看着的那张卡停在同一个 y,截到 0 为止;画上画的也是这个位置(从书本往回折时画的是你当时滚到的位置,这一条照旧)。来回开合之间什么也没滚、没换轮时,两边回到各自原来的位置,一个像素不差(记着上一次接的是哪一对位置)。
- 只保证正看着的那一张。 它下面的几张,书本里的右半比外屏的卡矮(卡上 Agent 的几行搬去了左页),整理段里它们上移补位(下表),所以不同高。
- 画先接好。 外屏停下来以后,在空闲时把书本与前板上的画先接到这个位置,翻开的第一帧不用量版面;翻开途中外屏还被滚动时,画跟着当帧改。
留下的,重排的。
| 留在原处 | 重排 |
|---|---|
| 日期、标题位置(右页上沿)、卡的顺序、你正看着的那张卡的位置(上面的规则)、作答按钮的竖直位置、回执、系统条;钉住区不滚 | 卡上 Agent 的几行里,标题行、档位、变化与份额搬去左页那一组的标题行,摘要与说明行不搬(左页写原话);右半压紧;“还有 N 件”展开成单行条;“进行中与结果”和次要事项搬到左页;累计栏换成左页页头的本轮计数与 158 格细格 |
两段。 Apple 的 API 说铰链角度用来驱动效果,布局交给排布变化。这里照做:翻页跟着铰链走,右页的布局在排布变成跨页时一次做完,板接着翻完。
翻页(跟手,θ = 铰链角度 0 → 180°)。 视角取发布会那种:你看着外屏,承载外屏的那块板(下称前板)绕它的左缘、也就是折痕,向左、朝你翻开。90° 时板侧立朝你;继续翻到 180° 落平,成为左页。前板的正面是外屏,背面是内屏左页,各自转开时就看不见(backface-visibility: hidden)。板下的右页始终是外屏那一页。函数都在 duo-motion.mjs 的 FOLD 与 foldAt(θ),下表的数由它现算;p、s、v 都截到 0–1,smooth 是 smoothstep。
| 量 | 随 θ 怎么变 | 0° | 54° | 90° | 108° | 144° | 180° |
|---|---|---|---|---|---|---|---|
| 前板转角 | rotateY(−θ),轴在折痕上;透视 2000 pt | 0 | 54 | 90 | 108 | 144 | 180 |
| 相机 | x = 折痕 + 半页 × (1 − smooth(θ / 180)),从外屏中心移到折痕,画面随之从右页取景改成跨页取景;y 在上起 22% 处 | 713 | 662 | 594 | 559 | 500 | 476 |
| 正面的不透明度 | 1 − p,p = (θ − 20) / (88 − 20):内屏接管(20°)之前不透明,侧立前淡尽 | 1 | 0.50 | 0 | — | — | — |
| 正面的磨砂 | 一层 10 pt 模糊的外屏,遮罩边从自由边向折痕扫到只剩 47 pt 清楚的一条(20° → 54°),上面一层纸色雾 0 → 0.5:透过它看见底下一模一样的右页 | 无 | 满 | — | — | — | — |
| 背面自由边的模糊 B | 16 pt(θ ≤ 90°),之后 16 × (1 − s)^1.5,s = (θ − 90) / (175 − 90) | — | — | 16 | 11.2 | 3.5 | 0 |
| 背面模糊的分布 | blur(u) = B × u³,u = 离折痕的距离 / 页宽(empirical.health:自由边 48 px @3x ≈ 16 pt,半程只剩八分之一)。画法:同一页叠三层,固定半径 2.5 / 7 / 14 pt,各自从 blur 过它半程的地方起由遮罩接上 | — | — | 3 层 | 3 层 | 1 层 | 0 层 |
| 背面的暗幕 | 自由边深(墨色 rgb 24 22 19)、往折痕渐透明的一层;88° → 168° 它的边从离自由边 0.92 页宽扫出页外(−0.36 页宽),不透明度 0.9 → 0.54,然后没有 | — | — | 0.89 | 0.80 | 0.62 | 0 |
| 右页 | 不动。0–20° 内屏还没亮,板下是暗的内屏,板正面仍不透明;20° 起右页亮起,就是外屏同一画面;120° 起换成内屏书本的右页,整理一次 | 外屏 | 外屏 | 外屏 | 外屏 | 书本 | 书本 |
| 份额带 | 横向缩放 smooth((θ − 120) / (170 − 120)),原点在折痕:从分界线向两侧长出来 | 0 | 0 | 0 | 0 | 0.47 | 1 |
| 组进卡的线、带下的账、卡上的小圈 | 线与份额带同一个缩放,原点也在折痕:向左长进组里、向右接进卡;账与小圈的不透明度同这个数。前板升起时画在背面那张清楚的副本上(模糊副本不画线:线挨着折痕,那里本来就是清的) | 0 | 0 | 0 | 0 | 0.47 | 1 |
所以展开时左边先看见的是变成毛玻璃、渐渐透明的外屏,然后板侧立;过了 90° 看见它的背面,靠折痕的一条先清,往自由边又糊又暗,像隔着一块铰接的毛玻璃;角度越开,清楚的部分越往外推,暗幕往外退,到 168° 暗尽、175° 糊尽。90° 时自由边离你最近;刚过 90°、背面转过来的 100–120° 之间它仍迎着你,左页显得比右页高。只有 transform 与 opacity 逐帧在变(遮罩是固定的渐变,靠平移挪位置),不重排。
停在半路(比如 70° 托在手里):画面就停在那一帧,可以松手,不重排。英雄数字不跟着重新取景:它在右页原位,件 → 份在整理段那一帧翻;Tech Talk 里时钟的重新取景,在这里由相机从右页移到跨页、份额带从折痕长出来承担,信息层不变形。
整理(定时 0.40 秒,排布变成跨页时触发一次)。 缓动沿用 D 的 cubic-bezier(.2, .75, .25, 1)。
| 时刻 | 什么 | 从 → 到 |
|---|---|---|
| 0–0.28 | 三条大条 | 右半压紧到书本里的高度:卡上 Agent 的几行离开(它们已在左页上),你的部件各自上移补位,“稍后”上到第一行末。左半一开始就在终点(前板背面画的就是书本的左页),不动 |
| 0–0.20 | 累计栏 | 右页底部的累计栏下沉 24 pt 淡出;左页页头的“158 条输入 · 累计 3051 条”与细格已在前板背面 |
| 0.08–0.36 | 单行条 | 右半从“还有 2 件”的位置展开;左半已在前板背面 |
| 0.12–0.36 | 右页下方 | 右页末尾那句“等你的就这 5 件。左页其余……不用你动。”上移 14 pt 淡入;左页下方的 Agent 在办 · 2 与“另有 7 项次要事项”已在前板背面 |
| 0.22 | 英雄数字 | “5 件等你回应”在同一位置一帧翻成“38 份等你回应”,副行“5 件 · 新问题 2 个 · 你在原处处理了 2 件”同时出现 |
| 120°–170° | 份额带、线与账 | 不在整理段里:随铰链从折痕向两侧长出(上表),分界线始终在折痕上 |
0.40 秒短于 Apple 锁屏的 0.7 秒:讲一遍里(跟手 1.5 秒)120° 在约 0.89 秒到,整理结束时板约在 176°,手还没停。减少动态时直接到终态。
合拢(开 → 合),倒放并加快。 排布离开跨页(往回折过 110°)时,先在 0.28 秒内倒放整理段的右页:“38 份”在 0.06 秒翻回“5 件”;单行条 0–0.20 秒收回“还有 2 件”;右半 0.06–0.28 秒展开回卡的高度。前板一直跟着铰链合回:份额带与组进卡的线 170° → 120° 向折痕缩回,暗幕与模糊从自由边扫向折痕(168° → 88°),侧立之后正面的外屏由透明、磨砂回到清楚,0° 时板压平,外屏就是 D,滚在书本上你正看着的那张卡的位置(上面的规则)。左页连同它下方的 Agent 在办,是前板背面的那张画,跟着板合上,不单独淡出。回执跟着条目走: 答过的那件,右半就是回执,它在右页上不移动;合拢时它所在的卡展开回 D 的“已答卡”(标题行 + 回执),回执还在你拇指下面。
3.4 第 2 拍:答一件(书本上)
2026-10-30 改。 之前这里写“卡高不变:右半收成回执后仍保持原来的高度,直到下一轮,所以这一行不变矮,下面不补位、不回流,也就不需要 D 的 0.4 秒手指保护;左页那一组折成一行,落到行底,贴着回执”。为的是两半同行、作答时不丢位置;代价是答完的那一行留着原高,回执贴在卡底、左页的一行贴在行底,中间一大片空,答两件就是两个空壳,用户截图看到的就是这个(1.8)。现在照 D 的 3a:那一行整体收起,收到 D 的已答卡那么高,下面的补上来。两半仍然同行同高,同的是收起之后的高度;位置也不丢:那一行的上沿不动,你正看着的就是它。
与 D 的 3a 相同的:卡原地收成回执、上沿不动;右半就是 D 的已答卡(同一个回执部件,标题一行 + 回执,同一个高度,83 pt);勾从 0.55 倍放大淡入;数字在 0.20 秒翻;下面的一次上移补位;折叠里的一件补上来;作答后 1.12 秒内的手指保护(按下处 32 pt 以内的轻点不落到滑过来的东西上,自己的回执照常能点)。不同在三处。一,收的是一整行:左页那一组同一拍、同一速度折成一行,和右半一样高,收的途中两半也一直同高。二,没有“还有 N 件”那一行:书本上所有问题已经都是条了,补上来的那件从它的单行条长成一行。三,数字是份:一次跳 24,不滚,一帧翻。
| 时刻 | 元素 | 从 → 到 |
|---|---|---|
| 0–0.20 | 右半 | 盒子原地收到已答卡的高度,上沿不动;期限、题干、你做过什么、按钮 0–0.12 淡出;标题一行与回执 0.10–0.30 淡入(手指下面):“你选了「免挖穿新管」· 00:00 / 已保存 · 等 Agent 下一轮整理,状态还没变”;勾 0.12–0.30 从 0.55 倍放大淡入;“撤回”在右端,焦点到它 |
| 0–0.20 | 左页那一组 | 同一拍、同一速度收成一行,与右半同高,名字与右半的标题在同一条线上:原话几行 0–0.12 淡出,0.10–0.30 写上“16 条输入 · 已折进你的回答,等 Agent 读到”;盒子由纸色换成只有一圈细边。数不变:下一轮整理之前,Agent 那边的事实不动(节拍守恒),折起的只是原话 |
| 0–0.18 | 进卡的线 | 向折痕缩回(横向缩放 1 → 0,缓入),淡在折痕上;卡上的小圈随卡的这一面一起换掉 |
| 0.20 | 英雄数字 | 38 → 14,一帧翻;副行加上“另 24 份你答过了,等 Agent 读到” |
| 0.20–0.40 | 份额带 | 主水管那 24 格就地由实变空心,仍在折痕右侧 |
| 0.20–0.52 | 下面的行 | 两半一起一次上移,补上收走的高度;线与小圈跟着各自的组与卡。领头换成下一件还在等的(D 的规则:答过的不再算三件之一),它的题干字号随之放大 |
| 0.20–0.52 | 补上来的一件 | 折叠里的下一件(f26 是 Signe 的同意书)从它的单行条长成一行:右半从单行条的盒子长成卡,左半从一行长成一组;单行条上没有、卡上才有的部分 0.36–0.72 淡入 |
撤回逆序、更快:0–0.20 秒那一行长回原高,下面的行同时让开,两半一直同高;那一组展开回原样,线 0.10–0.28 从折痕长回卡里;补上来的那件缩回单行条。时间在 duo-motion.mjs 的 BOOK.answer / BOOK.undo 与 FEED.fold / FEED.unfold。“稍后”同 D 飞向 ◷,份额照样减。合上时外屏就是 D,答一件是 D 的 3a 原样;桌面上的答一件见 4.1。
3.5 第 3 拍:整理落地(f22 → f23,书本上)
与 D 的 3b 同一张先后:先确认你做过的,再让份额让位。不同在于主角怎么走:D 是卡沉到下方成一行;这里是回执过折痕。左页的原料整页换成这一轮的:留下的组跟着自己的卡走,组里的原话交叉淡化成新的几行,线随组走、形状变了就交叉淡化;新出的组随卡进场,走掉的随卡离场;灰的几行与“折掉的”同样换掉。不飞点:一条输入落进哪件,看的是落地后它在哪一组,不画它飞过去。
| 时刻 | 元素 | 从 → 到 |
|---|---|---|
| 0 | 切点 | 日期、左页页头(“115 条输入 · 累计 2681 条”、115 格细格、“8 条成了右页的 2 件 · 26 条进了 Agent 那边 · 81 条折掉”)、右页说明行(“新问题 1 个 · 你在原处处理了 3 件”)、带下的账,一帧硬切 |
| 0–0.30 | Smilla 的回执 | 空心勾填实,小字换成“已收到 · Agent 在 12:00 的整理中读到” |
| 0.30 | 英雄数字 | 34 → 10 份,一帧翻;左页 6 → 29 份同一帧翻 |
| 0.30–0.70 | Smilla(主角) | 回执一次斜穿折痕,落到“Agent 在办”第 1 行的回执小字处;左半同时一次下移到这一行,盒子变成行(圆角 20 → 0);份额 24 → 12。目标在屏外时,它从左页下缘离开,左页页头的 29 份就是去处 |
| 0.30–0.50 | Mamma | 没答,问题随改写失效:右半淡出,留一行灰字“这个问题随改写失效”;左半 0.36 起下移到“已完成” |
| 0.30–0.70 | 份额带 | Smilla 的 24 个空心格一次左移跨过分界线、变灰、缩成 12;Mamma 的 24 格过线缩成 6;空余 25 格在左端长出来 |
| 0.40–0.72 | #13572 → 三个旧 PR、VAB、同意书 | 一次上移补位;#13572 的右半换成“三个旧 PR 推到哪天?”,标签换成“新问题”(左半标题同时换成“#13572已批准,另3个PR待审”) |
| 0.80–2.40 | 新问题 | 三个旧 PR 那一条的右半外圈描一次朱红 |
每个元素只走一段:回执是斜线一段,左半是竖直一段,两者同时到。滚动位置(2026-10-29 深夜): 落地在条目区里发生,钉住区里的日期、页头计数、细格与账只在 0 那一帧原地换掉,不移动;条目区跟 D 一样留住你正看着的东西:重画前记下视野里第一条(作答那一下记作答的那条),重画后把它放回原来的 y,各条的移位都从这里量;它若变成 Agent 在办的一行,视野就跟着它。目标在屏外时(回执落到左页下缘)不滚动去追它。问题被改写重登记(f26 答主水管 → f27):回执填实后,右半原地展开回问题,标签换成“问题已更新”,回执变成右半里的一行“你 00:00 答过上一版”;不过折痕,因为还轮到你。
3.6 两层与约束
- 信息层(约 99%)。 两页页头、带下的账、左页的组与原话、灰的几行、右页的卡、单行条、Agent 在办、次要事项、回执。墨黑加朱红;朱红只给能作答的东西(按钮、“新问题”、24 小时内的期限),而它们都在右页。左页没有朱红,因为左页没有要你做的事;折掉的更淡一层。
- 概念层(约 1%)。 64 格份额带(2 px 一格,一页宽)、左页页头的一排细格(本轮每条输入一格,按时刻排,高低三档是它的去向),和组进卡的线。份额带的分界线压在折痕上,答一件就有格子变空心,落地时空心格过线:这是“轮到谁”;线从原话汇成一股、穿过折痕接进卡,答完缩回:这是“从哪来”。只用墨和灰,不出数字。
- 克制。 翻页只换表现、不动信息层:板上画的就是两块屏本来的样子,糊与暗是隔着毛玻璃看它,不另加装饰。屏外是 iPhone Duo 的机身(3.7):它只是装两块屏的容器,三种姿态与翻页途中都在,但不进信息层,屏里的内容、位置、时间与没有机身时相同,屏的圆角与机身同心。机身的光只随铰链角度变(自由边的亮面与暗面、扫过两面的一道高光),不另加戏剧化的阴影与反光。平放时折痕处只有纸色留白,两半之间 24 pt,落在 HIG 的折叠区里,那里不放可点的东西;进卡的线从这里穿过,它不接点按。画面由数据构成:机身是装它的壳,屏里的东西不靠拟物。
- 为什么装上机身。 上一版这里写“板只有一圈发丝边……不画金属机身、铰链高光与戏剧化的阴影”。2026-10-29 晚,用户看过翻页后说“折叠屏需要 iPhone Duo 的那个手机壳呀”,接着又说“还原度高一点哈。或者网上有素材的话直接抄一抄。”翻开的那块板没有厚度、没有金属边,就读不出是一台手机在开合。于是这一句改掉:素材的许可不允许拿进仓库(1.2),就照 Apple 的渲染图量出机身自己画(3.7)。“画面由数据构成”留着:机身只装屏,不进信息层。
- 为什么改成 3D 翻页与模糊。 记录阶段这里写的是“不做 3D 翻页,左半不倾斜不旋转;不模糊;不画机身”,开合做成了平面擦除:左半当一扇窗,左缘从折痕往页边推,只变透明度。2026-10-29 用户看过后说“完全没抓到精髓,核心是那个动画”:iPhone Duo 开合的辨识度就在板真的绕铰链转、内容隔着毛玻璃由糊变清(1.2 的四条精髓),去掉这两样,剩下的只是一次换页。于是这一条改掉,只保留“不拟物”的那一半。
- 绝不动。 右页在翻页阶段的一切;英雄数字的位置;条的顺序(只表现
urgencyKey的结果);系统条。 - 圆角同心(2026-10-29 夜)。 每块屏的每个角都是机身那个角的半径减去两边的边框,合上时铰链一侧也是;内屏在折痕处没有角。要给系统条让位时,把系统条移进安全区,不缩小屏的圆角(1.6)。
- 一个元素一次一段,不折返;放大、缩小都并在同一次移动里。
- 不提前。 下一轮整理之前,左半不变、份额不动、回执不变实、空心格不过线。
- 预算。 翻页阶段逐帧变化的只有前板的转角(连同它的厚度,
translateZ)、相机位置、正面一层模糊的遮罩位置与纸雾、背面三层模糊的遮罩位置与暗幕、份额带与线的缩放、账与小圈的不透明度,加上机身自由边的亮面与暗面、两面各一道高光,共二十来个transform与opacity,不重排、不加模糊;机身其余部分是不动的一层,只在姿态或阶段换的时候换一副壳。整理段同时动的元素不超过 12 个。
3.7 机身(2026-10-29 晚)
几何在 duo-motion.mjs 的 BODY,量自 1.2 的渲染图,换到屏的比例画:内屏 951 pt 对约 157.7 mm,6.03 pt/mm。坐标是设备 pt,内屏左上角为原点。没有一张图:金属边、屏框、键与铰链盖都是 CSS 盒子,铰链处的缺口是一个内联 SVG,自由边的厚度是前板 3D 场景里的一条带和十二片小平面。
| 项 | 值 | 来处 |
|---|---|---|
一半的厚度 T | 31 pt(5.2 mm):前板的两面相距 T,自由边连起两面 | 规格 |
| 平放的机身 | 991 × 710 pt(x −20 … 971,y −18 … 692),164.4 × 117.8 mm | Netflix-app |
| 边框 | 自由边 20、上 18、内屏下 23 pt(外屏高 9 pt、两屏上沿对齐,外屏下边只剩 14)。由外往里:金属边连亮唇约 6.6 pt(七圈 inset 阴影叠出亮—暗—亮的抛光)、炭灰屏框(#313234),合计 16.7;像素前一圈黑 3.3 | Netflix-app |
| 圆角 | 机身 74 pt,合上时铰链一侧 24。屏与机身同心(1.6):内屏 54 / 56(上)、54 / 51(下);外屏外侧 54 / 56、54 / 60,铰链一侧 6.5 / 6、6.5 / 10;桌面是书本那一副转过来。系统条整条移进安全区(内屏右页在灵动岛的位置留空,时间在两块屏上都离上沿 52 pt),不再为它缩小圆角(2026-10-29 夜之前内屏取 36、外屏铰链一侧 10,与机身不同心) | Netflix-app、colors、display-sizes、HIG |
| 合上 | 前盖:黑玻璃屏框(#0c0c0e),从折痕往左多出 8 pt 的书脊,外屏装在里面 | colors |
| 键 | 只在手里那一半:上沿两颗(x 701–766、781–846),右缘一颗(y 179–297)与其下平齐的一颗(458–536),凸出 3 pt | Netflix-app |
| 铰链 | 折痕上下两端各一个金属边往里收的缺口,两块铰链盖 | Netflix-app |
| 天线断点 | 上下沿 x 69 / 380 / 565 / 874,自由边上 y 71 / 596 | Netflix-app |
| 没画的 | 相机凸台(在背面,正对屏幕的镜头里看不到);落影只有原来那一层 | — |
不动的一层。 .u-body 垫在两块屏下面,四副壳各自单独合成,按姿态与阶段换一副([data-body],换的只是 opacity),翻页途中一像素不动:
| 姿态 / 阶段 | 壳 | 前板 |
|---|---|---|
| 合上(0°) | closed:前盖连外屏,书脊与键从上沿、右缘露出 | 收起 |
| 翻页途中、半折静止(0° < θ < 180°) | right:手里那一半,从折痕到右缘,金属边在折痕处断开、接到前板上 | 升起,带着另一半的机身 |
| 书本(180°) | open:整台,折痕处两块铰链盖 | 收起 |
| 桌面 | table:书本顺时针转 90°,键转到右缘与下缘 | 收起 |
前板就是另一半。 正面是前盖(503.5 × 710 pt:书脊 8 + 半宽 495.5,连着外屏的副本),背面是这一半的内面(495.5 × 710 pt,连着左页的副本)。两面相距 T:rotateY(−θ) translateZ(T × θ / 180),合上时正面在 z = 0、平放时背面在 z = 0,所以 0° 与 180° 换壳那一帧不跳。自由边是一条 T 宽的金属带(rotateY(90°),绕外缘那条线转出来),四个圆角各六片小平面,都骑在前板上一起转,透视让它侧立时最宽。上下两条直边与铰链那一边,在正对屏幕的镜头里始终看不见,不画。
逐个角度(foldAt(θ).body 与 2000 pt 透视算出的;0° 与 180° 前板收起,没有自由边可看):
| θ | 27° | 54° | 81° | 90° | 108° | 144° |
|---|---|---|---|---|---|---|
前板的 translateZ(pt) | 4.7 | 9.3 | 13.9 | 15.5 | 18.6 | 24.8 |
| 自由边在画面上的宽(pt) | 11.9 | 29.7 | 40.8 | 41.2 | 36.6 | 13.9 |
| 自由边在画面上的 x(pt) | 943 | 784 | 515 | 416 | 227 | −9 |
亮面 sheen | 0 | 0 | 0.06 | 0.16 | 0.21 | 0 |
暗面 shade | 0.19 | 0.04 | 0 | 0 | 0 | 0.12 |
| 正面的高光 | 0.08 | 0.14 | 0 | 0 | 0 | 0 |
| 背面的高光 | 0 | 0 | 0 | 0 | 0.07 | 0.01 |
光。 自由边的外法线随板转,n = (cos θ, sin θ)(x 向右、z 朝你);主光在左上前方,它与视线的半程向量投到转动的平面上是 H = (−0.19, 0.94)。亮面 = 0.62 × max(0, n·H)^22,在 101° 最亮;暗面 = 0.5 × (1 − max(0, sin θ))^1.6,自由边越不朝你越暗。两面各有一道斜的高光,强度 0.16 × sin²:正面在 8°–84°(外屏还朝着你的时候),背面在 96°–148°(半折静止的 150° 之前就灭,正在读的页上不留反光);它在各自的窗口里横穿那一面一次。都只改 opacity 与 transform。
4 交互
4.1 姿态
| 姿态 | 屏幕(pt,假设) | 铰链 | Fold 画面 | 英雄数字 | 进入时怎么变 |
|---|---|---|---|---|---|
| 合上 | 外屏竖 466 × 678 | — | D 原样;系统条在右缘 | N 件 | 合拢倒放(3.3) |
| 帐篷 | 外屏横 678 × 466,立在桌上 | — | 一瞥:英雄数字、份额带、最急一件的问题与按钮(按钮在下沿,帐篷会晃);其余不显示。未画草图 | N 件 | 系统转屏,交叉淡化 0.25 秒 |
| 全开书本 | 内屏 951 × 669,每页 475.5 | 竖,居中 | 主画面:左页摊开的原料,右页你的卡,一组一卡同一行,线穿过折痕 | N 份 | 第 1 拍 |
| 半折书本 | 同上,两页成 V | 竖 | 同书本;折痕留白按 ReservedRegion(.division) 加宽,两半各自离开折痕,按钮不进折叠区;钉住区与滚动区同书本,斜着的左页上竖着拖,书本两页一起滚 | N 份 | 不重排,只改留白,跟着角度平滑 0.2 秒 |
| 桌面(笔记本) | 内屏转 90°,669 × 951;上半立、下半平放 | 横 | 条立成列:上半是摊开的原料(看),下半是你的卡(按),同一列同一件,一条竖线从组底越过横着的折痕落进卡的上沿。每列固定 191 pt 宽、不看内容,上下两半同一组列,一屏正好三列;还在等你的几件在前(D 的三件),答过的挪到最后;列多于三列时两半一起横滚、按列吸附,有更多的一侧显出纸色渐隐,屏底一排小点说屏上是哪几列(下文);其余几件只露右半,一件一列排在下半底行;Agent 在办写成组上面的一行,折掉的只在页头计数。上半的页头(日期、两组数、份额带、Agent 在办那一行)钉住,高度固定:副行左右各两行,长了用“…”截断,不折进份额带;上半的组与下半的卡各自竖滚,组与卡都从各自那半的顶上排起;竖线只在两半都在顶上时画,任一半滚开就淡出;转到桌面时两半都回到顶上、条带回到最左 | N 份 | 系统转屏后每条原地转成一列,顺序不变(答过的在最后),0.36 秒 |
| 全开竖 | 同桌面,平放手持 | 横 | 同桌面(铰链横着,左右的分界变成上下) | N 份 | 同桌面 |
全开竖和桌面用同一版式,是因为“分界跟着铰链走”:铰链横过来,原料与你的卡就成了上与下。为什么原料在上(上看下按,三星 Flex Mode 的做法):立着的半块屏离眼睛远、不好按,原话是拿来读的;平放的半块在拇指底下,作答的按钮在这里。两半的列宽对齐(下半也让出系统条那一栏),所以每条线都是竖直的一段,越过折痕就接进它那张卡;没有横穿的线,折痕上也没有可点的东西。灰的几行太多,立着的那半放不下,只在页头写“118 条折掉”,要看去书本。HIG 反对大幅重排,但这次转屏本身就是系统发起的;条的顺序、每件的两半、作答状态都不变,变的只是排布方向。唯一的例外是答过的几件:书本上它们原地收成一行矮条,桌面上排到最后(下文,2026-10-30)。
桌面的两半为什么各自滚、线只在顶上画(2026-10-29 深夜)。 上下两半是两块不同高的列表:立着的上半放组,组的行数有上限(6 / 4 行,其余并成“还有 N 条”),几乎总放得下;平放的下半放卡,加上只露右半的几件,composite 29 轮里有 15 轮比半屏高(最多多出 79 pt,story 没有)。一起滚就得让较矮的那半陪着滚,上半的页头会被推走;所以两半各自竖滚,横向则是同一条带子、一起滚(下段),并且用同一组列宽,第 k 组永远在第 k 张卡的正上方,列与列从不错开。线连的是“组的底”与“卡的顶”,两半各滚各的时候这两点的竖直距离不定,画出来要么断在半空、要么伸进别的卡;于是线只在两半都在顶上时画(按顶上的位置量好,是独立的一层),任一半滚离顶上,线与卡上的小圈 0.15 秒淡出,回到顶上再显。从书本转到桌面时两半都回到顶上,所以进桌面先看见线。
桌面的列宽为什么固定、两半一起横滚(2026-10-30)。 原来列宽按列数平分(minmax(0, 1.25fr) minmax(0, 1fr) …):三列时一列约 180 pt,答完一件,compose 照 D 把下一件从折叠里提上来,三列变四列,一列只剩约 131 pt,回执与题干一字一行(1.8)。窗口小时整台还要再缩一次。现在:
- 一列一个宽度,不看内容、不看列数。
TABLE.col= (669 − 56 − 2 × 12 − 2 × 8) / 3 = 191 pt:系统条那一栏让出,左右各留 12 pt,列间 8 pt,正好三列一屏。三列是因为 D 首页就是三张大卡(compose()的TOP):桌面上第一眼看见的正是等你的那三件。191 pt 里卡的标题两行、两三个按钮并排、回执一行半都放得下;两列一屏(约 290 pt)会把第三件推到屏外,四列一屏(约 140 pt)就是用户看到的挤法。上下两半是同一组列(同一个网格模板,同一个 x),组与卡、线与小圈永远对齐。 - 放不下就横滚,不压窄。 列多于三列时条带向右伸出屏外。上下两半在同一个横向滚动容器里,一起横移,所以第 k 组仍在第 k 张卡的正上方,竖线仍是竖的;各自的竖滚不变。松手吸附到最近一列的左沿(
scroll-snap-type: x mandatory,吸附点是条带里一排看不见的标记,各在一列的左沿、留出 12 pt 的边:竖滚的两半各自是滚动容器,吸附点不能放在列本身上)。屏外还有列的那一侧,边上一道纸色渐隐和一根发丝线(钉住区分界那一条,竖过来);屏底一排小点,一点一列,墨色的是屏上的三列。横滑落在上半或下半都滚条带,竖着的手势仍归那一半;滚轮的横向分量落在页头上也滚条带。 - 窗口只改缩放。 整台按 pt 画、等比缩进视口(5 节),窗口大小只改缩放,不改 pt 版式:1180 × 760 下缩到 0.63,仍是三列 191 pt,屏上一列约 120 px;没有“窗口窄就少放一列”这一档,因为模拟的是一台固定尺寸的机器。
- 答过的列宽不变、挪到最后。 答一件,那一列不变窄:下半的卡收成 D 的已答卡(标题一行加回执;列窄,“撤回”挪到标题那一行的右端,回执的字用整张卡的宽),贴在下半的顶上;上半那一组折成一行,贴在列顶,与书本左页那一行同一个标记。然后这一列挪到条带的最后,还在等的几件向左补上,从折叠里提上来的那件在第三列原地长出来(不从下半底行的单行条斜穿过去)。为什么挪:一,手边先放要做的。书本上答过的一行收到 83 pt,等着的几件就上来了;桌面上一列只能在高度上收,宽度省不出来,答过的一列几乎是空的(一行折起的组、一张回执),却占着三分之一屏,第三件等你的就被挤到屏外。挪到最后,屏上仍是等你的三件,与 D 的三张大卡同一个意思。二,不丢:回执与“撤回”还在,往右滚就看得到,小点说后面还有几列;撤回时那一列回到原处,条带跟着它滚回屏上。三,不打手:那一列先在原处收起(0–0.20 秒,回执在手指下面),停一下(到 0.40 秒)再挪(0.40–0.76 秒),1.12 秒内按下处 32 pt 以内的轻点不落到滑过来的卡上。代价是桌面与书本的顺序不再一样(答过的几件在书本上原地、在桌面上最后),转屏时它们换了位置。考虑过、没有用的:原地不挪(宽度固定后不再挤,但第三件等你的就在屏外,要滚才看得到);把答过的一列收窄(列宽不一,吸附与小点就不再是一列一格)。
4.2 手势
标准与 D、ix 相同:手机没有悬停,轻点直接起作用,长按代替悬停;轻点展开详情;Esc、点空白处收回;回执跟着条目走。书本专属的地方用 ★ 标出。
| 手势 | 在哪 | 对应 ix 标准 | 动效 | 时长 |
|---|---|---|---|---|
| 轻点按钮 | 右半、单行条 | 作答 | 那一行整体收成 D 的已答卡,左页那组同高,下面的上移补位(3.4);桌面上那一列收起后挪到最后(4.1);作答后 1.12 秒内按下处 32 pt 以内的轻点不落到滑过来的东西上 | 0.72 秒;桌面 0.92 秒 |
| 轻点“撤回” | 回执 | 作答的逆 | 逆序:那一行长回原高,下面的同时让开;桌面上那一列回到原处,条带跟着它滚回屏上 | 0.36 秒 |
| ★ 轻点左页一组 | 左页的组、单行条的左半 | 点击 → 展开详情 | 组沿自己的框外扩 8 px,再长满整个左页,写这一组的原文:每条的人、来源、时刻、主题与全文(数据里留的至多 500 字);末尾“它为什么在这里、怎么变到现在 ›”在同一块板上换成 D 的详情(完整走势、来源、上一版回执)。右页不动、仍可作答,这就是伴随窗格。其余降到七成 | 0.44 秒;收回 0.36 秒 |
| ★ 轻点一行灰的 | 进了 Agent 那边的一行、折掉的一行 | 同上 | 那几条输入的原文开在左页,同上;暂缓、噪音、重复的另写处置。换轮时收起 | 同上 |
| 轻点右半空白处 | 右半 | 同上 | 这件事的详情(D 的那一份),从它那一组的位置长出来,也开在左页,不盖住按钮 | 同上 |
| 长按 0.45 秒 | 单行条、Agent 在办的行、次要事项、页头累计行 | 悬停 → 概要卡 | 行外扩 8 px,概要卡贴行下沿出现,写 itemFacts 的同一组字段 | 0.15 秒;收 0.12 秒 |
| ★ 长按色带格 | 份额带 | 悬停 → 概要卡 | 格所属那件的条描一道墨框(跨折痕联动,同 Fantastical);概要卡贴着色带 | 0.15 秒 |
| 左滑过 80 px | Agent 在办的行(外屏与书本) | 知道了 | 跟手;松手过 80 pt 就是“知道了”,不到回弹 | 跟手;回弹 0.20 秒 |
| 下拉(内容已在顶部) | 左页详情 | Esc → 收回 | 跟着手指下移、变淡;过四分之一或快拉收起,不然弹回(5 的出入表) | 跟手;收起 0.16 秒、弹回 0.20 秒 |
| Esc / 点空白 / 再点同一条 | 左页详情、概要卡 | 收回 | 同上 | 0.36 秒;概要 0.12 秒 |
| 竖向滚动 | 书本两页一起;外屏;桌面上下两半各自 | — | 只有条目区滚,页头、份额带、账钉住(4.5);一张跨页一起滚,条的两半永远同一行;左页下方的“Agent 在办”在最后几条之下;滚到头不带动整页;钉住区下沿的分界只在滚动时显 | 系统 |
| ★ 横滑、横向滚轮 | 桌面的上半或下半(滚轮也可在页头上) | — | 两半一起横移,松手吸附到最近一列的左沿(留 12 pt 边);屏外还有列的一侧显出纸色渐隐,屏底小点跟着变(4.1);滚到头不带动整页 | 系统 |
| 滚轮 | 整块屏 | — | 落在列表上由列表自己滚;落在钉住区、系统条、前板或折痕上,滚这块屏的列表;落在详情或操作单上滚它们自己;整页不动 | 系统 |
| ★ 轻点折痕的折叠区 | 折痕两侧各 12 pt(半折 24 pt;桌面在横折痕上) | — | 什么也不做;浏览器把落在这里的点按挪到旁边那一组时也不算 | — |
| 轻点“记录” | 系统条(三块屏同一处) | — | 我的回应记录(D 的操作单):本页作答可撤回;故事里在原处做过的标 ↩;“看这件事”开它的详情 | 0.30 秒 |
| ★ 合上 / 展开 | 铰链 | — | 3.3;中途松手停在当前帧 | 跟手 + 0.40 / 0.28 秒 |
| 桌面:轻点下半 / 上半 | 列 | 作答 / 展开详情 | 作答同上;详情长满上半 | 同书本 |
细则:
- 详情开在左页,不开成全屏。 这是书本相对 D 的主要收获:读证据和按按钮在同一眼里完成。详情打开时右半仍是这件事的问题。在详情里作答,回执落在右半,详情不关。
- 两页一起滚。 不做左右独立滚动;独立滚动会把一件事的两半错开,拆分就失去了意义。页头、份额带与账不滚(4.5)。
- 切换打断。 换轮、开合、转屏时,详情与概要先立即收起,再照常执行(与 ix 的打断规则相同)。开合指铰链离开书本的 110°–180°:半折、退回 120° 以内都不打断,往回折过 110° 时收起。滚动一下也收起长按出的概要卡(它是贴着那一行摆的)。
4.3 与 D、ix.mjs 的关系
| 共用 | 内容 |
|---|---|
| 排序与集合 | D 的 compose() 与 urgencyKey,一字不改 |
| 作答与回执 | respond.mjs;回执已保存 → 已收到,跨版本跟着条目走 |
| 概要字段 | dial-model.mjs 的 itemFacts |
| 交互常量 | ix 的 pop 8 px、dim 七成、fadeIn 0.15、fadeOut 0.12 秒;打断规则;减少动态 |
| 脚本接口 | window.foldDuo 用与 foldHome 同名同义的 hover / unhover / open / close / answer / undo / facts / top / waiting / ixAt / ixState,再加 posture(name)、hinge(角度)、unfold(秒)、fold(秒)、state()、tell,供定格对照与检查(第 5、6 节) |
Duo 专属:铰链角度驱动的翻页、排布触发的整理、跨页里一行的两半、详情开在左页、份额带分界压在折痕、姿态之间的转换。
4.4 定下的四点
记录阶段留下四个待定点,2026-10-28 由用户定下(经协调者转达),实现照此:
- 展开时数字翻成份。 在整理段 0.22 秒那一帧从“件”翻成“份”;外屏仍写“件”,合上时翻回去。
- 单行条也有左半,只一行:事项名、份额、本轮进了几条。这样“每件事都沿折痕拆开”没有例外。
- 帐篷姿态这一版不做,留在记录里(4.1 表),等真机确认帐篷时外屏能不能作答。
- 角度阈值做常量:接管 20°、报告跨页 120°(往回折过 110° 退出,留 10° 回差),都待真机量(见 1.1“未证实”)。
4.5 钉住区与滚动区(2026-10-29 深夜)
| 屏 | 钉住 | 滚动 | 分界 |
|---|---|---|---|
| 外屏(合上) | D 的页头:日期、“N 件等你回应”、说明行与工具 | 卡、单行条、Agent 在办、次要事项;底部的累计栏仍是 D 的贴底栏,贴在滚动区的底上 | 页头下沿 |
| 书本(180°;半折同) | 系统条;两页页头(左:本轮、“158 条输入”、细格、去向一行;右:日期、“38 份等你回应”、副行)、份额带、带下的账 | 一张跨页:每一行的两半(组与卡)、穿折痕的线与卡左缘的小圈、单行条、左页下面的 Agent 在办与折掉的、右页末尾那句 | 一条,横过两页 |
| 桌面 | 系统条;上半的页头(日期、“38 份”、本轮计数、份额带、Agent 在办那一行),高度固定 | 竖向:上半各组、下半各卡与只露右半的几件,两半各自滚;横向:两半是同一条带子,一起滚、按列吸附(4.1) | 上半页头下沿;下半的顶边(横折痕);横向有更多的一侧的竖边 |
| 左页详情、操作单 | 详情的返回栏 | 详情正文(D 的);操作单整张 | — |
规则:
- 钉住区不动。 滚动时页头、份额带、账一像素不动。份额带从折痕长出(120°–170°)仍是它自己的缩放,钉住区只是不跟着滚;钉住区里的数在节拍上原地换。
- 书本两页一起滚。 书本只有一个滚动区,横跨两页、在钉住区下面,所以组与卡永远同行,线与小圈也在里面跟着走。
- 分界只在滚动时显。 钉住区下沿一条 14 pt 的纸色渐隐,上缘一根 6% 墨色的发丝线;条目滑到它下面时 0.18 秒淡入,回到顶上就退。不加阴影。
- 一个手势只滚一个列表。 每个列表滚到头都不带动整页(
overscroll-behavior: contain);从钉住区、系统条、累计栏或前板上开始的拖动不交给页面(touch-action: none);滚轮落在一块屏的任何地方都滚这块屏的列表(见 4.2)。 - 桌面的列不错开。 两半各自竖滚、一起横滚,列宽固定且相同(191 pt);线只在两半都在顶上时画;转到桌面时两半回到顶上、条带回到最左(4.1 表后)。
- 半折同书本。 静止在 120°–170° 时左页是前板上的画:页头固定在画上,条目那一层按书本的滚动平移;手指在斜着的左页上竖着拖,书本两页一起滚;滚轮同上。
- 开合不丢位置。 外屏与书本按你正看着的那张卡接上(3.3);[ ]、F、拖把手、半折、退出跨页再回来,书本与外屏的滚动都不变(Android Tier 3 连续性,1.4)。讲一遍开场时两块屏都在顶上。
- 换轮、作答不丢位置。 你正看着的那一条留在原来的 y(3.5);作答时那一行收起、上沿不动,下面的上移补位(3.4;2026-10-30 之前是卡高不变、下面不动);桌面上答过的列挪到最后,撤回时条带跟着它回来(4.1)。键盘 Tab 的顺序与看的顺序相同:钉住区在前,然后每一行先左半、后右半;焦点移到的条目由它所在的列表滚进视野,不会落在页头底下。
按用户的操作顺序走一遍。 一个真用手机的人从合上开始:滚外屏、点卡作答、看回执、开详情、翻开、在书本上滚、开一组原文、在右页作答、半折、转成桌面、再合上。走下来修掉的地方:
| 场景 | 原来 | 现在 |
|---|---|---|
| 合上,滚外屏 | 页头随 D 的整页一起滚走 | 页头钉住:E 的外屏把 D 的页头拆到滚动容器外(D 本身不改) |
| 合上,滚动时开着的概要卡 | 留在原处,和它那一行错开 | 收起(书本上原来就这样) |
| 合上,点底部的累计栏 | 看着能点,点了没反应 | 本轮的原文开在外屏上:每条输入一段,各写上去向(进了哪张卡、Agent 那边、暂缓 / 噪音 / 重复),按时间排;再点或 Esc 收起。翻开看的是同一批原料按卡分好的样子 |
| 合上,左滑 Agent 在办的一行 | 4.2 写了,E 里没做 | 照 D:跟手,过 80 pt 松手就是“知道了”(书本左页下面的行也一样) |
| 滚过外屏再翻开 | 书本总从顶上开始,外屏上看着的那张卡要在右页上重找 | 接着读(3.3) |
| 书本,滚动 | 页头、份额带、账一起滚走 | 钉住,只有条目滚 |
| 书本,详情开着时转铰链 | 详情与概要卡留着,跟着前板翻上去 | 离开 110°–180° 时先收起(4.2 细则本来就这么写,没做) |
| 书本,下拉左页详情 | 4.2 写了,E 里没做 | 正文在顶上时下拉:跟着手指下移变淡,过四分之一或快拉收起,不然弹回 |
| 书本,轻点折痕 | 浏览器把落在折叠区里的点按挪到旁边那一组,打开了它的原文 | 什么也不做,挪过去的那一下也不算 |
| 书本,把手 | 36 pt 宽,压到两边条目的边上 6 pt | 24 pt,与折叠区同宽 |
| 半折,在斜着的左页上拖 | 前板是画,拖不动 | 书本两页一起滚 |
| 桌面,下半放不下 | 不滚,多出的卡(composite f03、f04 多 79 pt)被裁掉、点不到 | 两半各自滚,页头钉住,列不错开,线只在两半都在顶上时画;滚开时横折痕处显出分界 |
| 桌面,上下的列 | 都是 1.25fr 1fr 1fr,内容长的一列被撑宽,上下错开约 2 pt,竖线对不准卡上的小圈 | minmax(0, …),上下同宽;2026-10-30 改成每列固定 191 pt、两半一起横滚(4.1) |
| 桌面,答一两件以后(2026-10-30) | 四列挤成一列约 131 pt,回执一字一行、“撤回”压在字上;上半的组折成一小块贴在列底;页头副行折成两行把份额带推下去 | 列宽不变;卡收成回执、组折成一行,都贴在列顶;答过的列挪到最后;页头每行一行,长了截断 |
| 书本,答一两件以后(2026-10-30) | 答过的一行保持原高,回执贴卡底、组的一行贴行底,中间空着 | 整行收成回执高,下面的上移补位(3.4) |
| 各处,滚轮落在页头、系统条或前板上 | 宿主页面跟着滚 | 滚这块屏的列表 |
| 各处,系统条上的“记录” | 画着,点不了 | 打开我的回应记录;被稍后的事在这里找得到 |
没有改的:讲一遍各拍照旧(开场时两块屏都放回顶上,整理段不再需要“把滚动放在顶上”:外屏在顶上,书本就在顶上);任意键跳到结尾、减少动态直达终态不变(减少动态时开合也按 3.3 接上滚动位置,只是不动画)。
5 实现与出入
文件。 variants/duo.mjs 编排:三种姿态、铰链、各拍、手势、讲一遍与 window.foldDuo;variants/duo-motion.mjs 只放几何、阈值与各拍的时间表(第 3 节的数在这里一一对应);variants/duo.css 是版式;D 的 d.css 与 ix.css 由 duo.mjs 在挂载时以样式链接插入,两张样式表都到了才显示第一帧(不用 @import:合页导出把样式链接内联成 data URL,样式表里不许有 url()),卡、标签、按钮、回执、行、详情与操作单都是 D 的。D 的视图逻辑(作答记录怎么读、compose() 与 urgencyKey、卡与行的标记、页头与说明行、详情内容)抽成 variants/d-view.mjs,D 与 E 共用,D 的画面逐像素不变;“一组怎么动”的部件直接用 d-motion.mjs 的 morph / reveal / band / ghostOf / stepAt。shared.mjs、scenario.mjs、engine.mjs、respond.mjs 都没改。壳层 explore.mjs 与 index.html 只多一项 duo(切换栏写 E)。左页的去向规则在 variants/inflow.mjs,C 与 E 共用:它是从 c.mjs 原样搬出来的(2026-10-29 夜),C 改成从这里取,画面与数逐字节不变。
浏览器里的铰链。 按 pt 画整台(外屏 466 × 678、内屏 951 × 669),等比缩进视口。把手在折痕上,拖一页宽是 180°,跟手;松手时不到 90° 落回合上,过 90° 落到书本(半圈 0.36 秒,缓出)。[ ] 每次 15°,F 一键折 / 展,侧栏三个按钮切合上、书本、桌面;?hinge=h 定格在 θ = 180° × h(0 合上、0.5 前板侧立、1 全平;2026-10-29 之前它是揭开进度,0 → 1 对应 20° → 120°),?posture=closed|half|table 直接进那个姿态。170° 以下算半折:两半之间的留白从 24 pt 加宽到 48 pt(0.2 秒),不重排;拖动或连播中途经过 170° 不改留白,停下来才改。
开合怎么画(3.3 的表)。设备容器里一层 perspective: 2000pt 的 3D 舞台,前板 rotateY(−θ)、轴在折痕。前板上是一张“画”:外屏与内屏左页的副本(去掉键、ID、焦点与一次性标记),在前板升起时拍下;最近一次重画 0.7 秒以后、没有节拍、拖动或铰链在动时,用空闲回调提前拍好,所以翻页的第一帧不用建 DOM。两块屏的内容签名变了(换轮、作答、换姿态)才作废重拍;还没拍好就开始翻时在第一帧现拍,4 倍降速下这一帧约 67–83 毫秒。正面 = 外屏副本 + 一层 10 pt 模糊副本 + 纸雾;背面 = 左页的清楚副本 + 三层模糊副本(2.5 / 7 / 14 pt)+ 暗幕。每层模糊各有一张固定的渐变遮罩,宽两页,靠一对相反的平移挪边的位置,所以逐帧只改 transform 与 opacity。左页副本的右栏换成同高的空盒,每一行的高度与书本一致。前板升起时内屏书本只画右页(clip-path),左页就是前板的背面,内屏的左栏点不到;落平(180°)时前板收起,书本照常整页画,与改之前逐像素相同(第 6 节)。半折静止时前板停在那个角度,左页是斜着的那张画:它的清楚那层保留条目的 data-id,接收轻点与长按,走同一套委托;左页详情平铺在它上面。左页副本照内屏分两层:钉住的页头一层不动,条目那一层(.u-spread 的副本)按书本的滚动 translateY,两页一起滚时只改这一个 transform。滚动位置按 3.3 接上:外屏在顶上时左页从顶上读起;从书本往回折时,画的是你当时滚到的位置。脚本接口:foldDuo.state() 原来的 windows 换成 panel(前板的转角,收起时为 0)与 fold(这一帧正面的不透明度、背面自由边的模糊 pt、显示的模糊副本数、暗幕的不透明度与边的位置、份额带的缩放,与 foldAt() 同一组数);unfold() / fold() 用手的缓动。
机身怎么画(3.7)。duo-motion.mjs 的 bodyHTML()、panelBodyHTML() 与 pageCutHTML() 是纯函数,出四副不动的壳、键、前板两面的边框、自由边和右页的下缘;duo.mjs 挂载时插入一次。不动的那层 .u-body 垫在屏下面,四副壳各是一个合成层、只画一次,显示哪一副由 data-body 定、靠 opacity 换,前板升起或落下时换壳不重画;哪一副壳用 data-shell 标,不用类名,因为页面会把整个宿主里的 is-open 一并清掉(打开详情时)。转屏时只把离开的那副壳复制出来,随旧排布一起淡出。前板两面各是“边框 + 屏的副本”(副本的容器从 .u-face 挪进 .u-glass,圆角跟机身的屏),自由边是带与小平面,两面的高光只是一条 226 pt 宽、斜 14° 的带,平移着扫过去。逐帧多改的只有自由边亮面、暗面两层的 opacity 与两道高光的 opacity、transform(bodyAt(θ)),不加模糊、不重排。外屏在翻页途中当右页时比内屏右页高 9 pt:一层盖板(.u-pagecut)按外屏的形状裁,在内屏右页的轮廓外画上与机身同色的黑边与屏框,只淡入淡出,所以 120° 换成内屏时右页的下缘不跳,也不改任何裁切。前板收起时不再 visibility: hidden,而是透明(不加 will-change,合成器不画它,模糊也不算):它一直画好着,翻开的第一帧不用现画边框与副本;收起时它接不到点按(只有升起、静止的前板接)。整台按 pt 等比缩进视口时,把机身伸出的部分(键、左半、落影,BODY_PAD)也算进去,所以 1440 × 900 下屏比 #42 小 3.6%(内屏 1046 → 1009 px 宽)。屏上浮着的详情、操作单与回执过折痕的那层(.u-clip)按屏的圆角裁。脚本接口:foldDuo.state() 加了 body,shell 是哪副壳,light 是前板升起时自由边的亮面、暗面与两道高光(收起时为 null);原有字段一个不少。
左页怎么画(2026-10-29 夜,2.1)。duo.mjs 的 feed() 按帧缓存 inputsOf(i):每条输入的时刻、处置与落进的根层事项。一组是 .u-half.is-left.is-feed,与右半同在一个网格行里(align-items: stretch,同一行、同一高);组里每行的高度是定死的(FEED:上内边距 12、标题行 20、第二行 18、间隔 6、行距 19 pt),所以线从哪几行出发、在哪汇成一股,由标记算出来,不量版面。线是组里的一个内联 SVG(.u-link,data-part="link",desc 记它的形状,换轮时形状变了就交叉淡化),从组的右缘往里 30 pt 起,伸出组 56 pt,穿过折痕钻到卡下面(卡在它上面画),卡的左缘一个小圈是它接进去的地方;它不接点按(pointer-events: none)。线、账与小圈随铰链长出:applyGrowth() 每帧只写它们的 transform(scaleX,原点在折痕:transform-origin: calc(30pt + 折痕宽 / 2) 0)与 opacity。前板升起时,左页是前板背面的画,线只画在清楚的那一层上;三层模糊副本拍画时把线去掉、把账藏起(保留它的行高),所以逐帧多写的只有三条线、三个小圈与一行账,模糊层里没有逐帧变的子元素。答一件时 choreograph 多了两个钩子:onGhost 让旧那一面上的线缩回折痕,onEl 在撤回时让新的线长回来;组本身照 FEED.fold 的时间与卡一起走 morph。半折(170° 以下静止)时折痕宽 48 pt,线伸出 56 pt 仍够得着卡。桌面姿态的线是一根竖直的 1.6 pt 墨线(.u-link.is-down),从组底越过横折痕落进卡上沿的小圈;两半都让出系统条那一栏,列宽相同(4.1)。详情:openDetail(id, origin, mode),mode 是 feed(原文)或 item(D 的详情);灰的几行不是事项,用 gone:<id> 与 fold:<处置>:<来源> 作键,只在本页持有,不进模型的选中,换轮时收起。脚本接口:foldDuo.facts() 加 feed(这一组几条、折起没有、有没有线、组与卡的上沿),foldDuo.census() 是页头那组数,foldDuo.state() 加 links(几条线、长到几成)与 panelMode;原有字段一个不少。
滚动怎么做(2026-10-29 深夜,4.5)。内屏是 .u-book:上面 .u-pin 放两页页头、份额带与账(同一个两栏网格),下面 .u-iscroll 是唯一的滚动容器,里面是 .u-spread;键控的 patch() 分别更新这两个列表,离场的旧元素从各自所在的列表飞走(choreograph 的 exitTo)。外屏是 .u-ocol:D 的页头 header.d-head 在 .u-oscroll(D 的 .d-scroll)之外,D 的累计栏仍在滚动区里贴底;D 的 d-view.mjs、d.mjs、d.css 一个字节没改。桌面是 .u-tmain = .u-tpin(页头)+ .u-tview;.u-tview 里的 .u-tcols 是横向的滚动容器(条带),两行网格,上行 .u-tscroll(组)、下行 .u-bot(卡)各自竖滚,竖线在两半之间独立的一层 .u-tlinks,也在条带里,按两半都在顶上时量(2026-10-30 起,见下一段)。分界是钉住区的 ::after(纸色渐隐加 6% 发丝线),列表滚离顶上时由一个类名淡入,桌面下半的分界是 .u-table::after。开合时的接法:pickAnchor 找视野里第一条两块屏都有的、露出至少 24 pt 的条目,carryToBook() / carryToOuter() 按 3.3 的式子算出另一块屏的 scrollTop,carried 记住上一次接的那一对位置,没滚、没换轮时原样还回去;合上时外屏一停,空闲回调里就把书本与前板的画接好。换轮与作答时 keepAnchor 照 D 的做法把记下的那条放回原来的 y。滚轮由设备上一个非被动的 wheel 监听转给这块屏的列表;半折时前板上的竖拖由指针事件改书本的 scrollTop。滚动只写 scrollTop、一个类名和前板副本的一个 transform,先读后写,不逐帧量版面。foldDuo 加了 scroll(层, y)(层是 book / outer / top / bottom)与 state().scroll。
答一件怎么做(2026-10-30,3.4、4.1)。右半已答时的标记是 D 的已答卡:<p class="d-kicker" data-part="title"> 一行标题加 receiptFor() 的回执(d-view.mjs,同一个部件),外边距与 D 的 .d-card.is-answered 相同,所以同一件、同一个回答时高度一样(composite f26 主水管:两边都是 83 pt)。原来为“卡高不变”记下每张卡原高、作答后写成 min-height 的那张表(held)删掉了;书本的网格行仍是 align-items: stretch,左页折起的那一组(.u-half.is-left.is-folded)是一行、没有纸色底,于是这一行的高就是回执的高。compose() 不改:答过的仍在 zone 里、不算三件之一,领头换成第一件还在等的(modeOf(),与 D 同一条规则),下一件照 D 从 more 提上来。节拍是 choreograph 原来那一套 FLIP(morph:移位、三段式盒子、部件平移或交叉淡化、旧那一面的影子),只是把时间分成三张表:作答那一件的两半用 BOOK.answer.self(0–0.20,左半是 FEED.fold,与它同一拍),其余用 others(0.20–0.52),新出现的用 enter;撤回是 BOOK.undo,两张表同时走。勾照 D 的 3a 从 0.55 倍放大淡入(popCheck,外屏也一样)。手指保护照 D:作答时记下按下的位置,1.12 秒内落在它 32 pt 以内的轻点在宿主上被截下,除非点的就是自己的回执。桌面:TABLE(duo-motion.mjs)定列宽 191 pt、列间 8、边 12;tableEntries() 先排还在等的、再排答过的;renderTable() 在列数变了时改 --n 与条带宽 --tw、重写吸附标记与小点;作答那一列的两半用 self,移位用 strip(0.40–0.76),竖线跟着它那一列平移,答过的那条先缩回、新列的最后长出;撤回时 showColumn() 把那一列滚回屏上。页头的副行是定数的几行(.u-tlines,每行 18.75 pt、不折行、长了用“…”),Agent 在办那一行没有在办的事时也写一句,所以钉住区一轮一轮都是同一个高(166 pt)。脚本接口:foldDuo.scroll('cols', x) 与 state().scroll.cols;原有字段一个不少。
讲一遍。 顶层直接打开、地址里没有 frame / posture / hinge、没有 referrer 时演一遍,约 11 秒:合上 0.9 秒 → 跟手展开 1.5 秒(加整理 0.40 秒;“跟手”用最小加加速度缓动,起落都软、侧立时最快)→ 停 1.6 秒 → 答一件 → 2.0 秒后下一轮落地 → 2.8 秒后合上 1.1 秒。composite 是 f22 答 Smilla“打给诊所”、落到 f23(与 D、A 同一个样本);story 是 f04 答导出模板“固定模板”、落到 f05。任意键或点按跳到结尾(合上、下一轮、已答);侧栏“重演”从头演。嵌在合页或体验站的弹框里、从别处链进来、带 frame / posture / hinge 打开都不演;不带帧号时停在 composite f26、story f04(四件等你、其中一件是单行条;f14 只剩一件)。减少动态时不演,折、展、作答都直接到终态。
与故事的出入。
| 处 | 故事(第 3、4 节) | 实现 | 为什么 |
|---|---|---|---|
| 整理段的单行条 | 0.08–0.36 从“还有 2 件”的位置一件件展开(错开 8 pt) | 右半从“还有 N 件”那一行的盒子长出来,同时到,不错开;左半在前板背面,不滑入 | 一件件错开时显得零碎;左半见下一行 |
| 整理段的左半(2026-10-29) | 跟着这一行一起变矮;累计行、Agent 在办 0.12–0.36 淡入 | 一开始就在终点,整理段里不动 | 左页是最终跨页的左页,钉在终点(1.2 精髓三);整理段里再动就不是钉住了 |
| 份额带(2026-10-29) | 整理段 0.20–0.40 淡入,最后到 | 120°–170° 随铰链从折痕向两侧长出 | 精髓四:跨折痕的东西随铰链重新取景,不在最后淡入 |
| 英雄数字的取景(2026-10-29) | — | 不沿折痕重新取景,留在右页原位,件 → 份仍在整理段 0.22 秒 | 它量的是你这边的份数,属于右页;挪到跨页中间会压在折痕上、离开它所量的那一侧,信息层变形。取景的变化交给相机从右页移到跨页 |
| 整理段的右半 | 你的部件各自上移补位 | 题干照 D 的换字号做法(同词缩放,折行变了就交叉淡化),期限、标签、按钮、说明行整块平移、不交叉淡化 | 按钮交叉淡化时两组按钮同时半透明,像点了两次 |
| 合拢的左页下方 | 0–0.16 秒淡出 | 不单独淡出:它在前板背面那张画上,跟着板合上 | 2026-10-29 改成翻页以后,左页整页就是前板的背面 |
| 落地的先后 | 主角 0.30–0.70,其余上移 0.40–0.72,Mamma 右半 0.30–0.50 淡出 | 主角 0.30–0.70(缓入缓出),其余 0.34–0.70,失效与离场 0.28–0.40 | 按故事的时刻,主角与 Mamma 走得快,0.40–0.45 秒上方空出一整块;提前 0.06 秒、离场缩短后空白只剩一闪 |
| 回执过折痕 | 回执斜穿折痕 | 回执套一块纸色小签斜穿,“撤回”不跟着走;目标在屏外时落到左页下缘、在最后三成淡出 | 光是一行字时,它越过别的条时读不出来是一样东西 |
| 失效的问题 | 右半淡出,留一行灰字 | 右半淡出;灰字“这个问题随改写失效”留在它变成的那一行里(D 的行标记) | 右半走了之后没有地方留灰字,D 的行本来就写这一句 |
| 答一件 | 空心勾 0.12–0.30 描出 | 回执随标题一行 0.10–0.30 淡入,勾照 D 的 3a 从 0.55 倍放大淡入(2026-10-30 之前勾不单独动),不描边 | 用 D 的同一个回执部件与同一个动作,没有另做描边 |
| 桌面答一件(2026-10-30) | 4.1:那一列收起后挪到最后,提上来的一件补到第三列 | 提上来的那件在第三列原地长出,下半底行它原来那张只露右半的小卡淡出,不从那里长过去 | 底行的小卡与第三列隔着整屏,从那里长过去是一笔斜穿,和挪列的方向交叉 |
| 稍后提醒 | 同 D 飞向 ◷ | 条淡出离场,份额照样减;外屏页头没有 ◷ 计数;被稍后的事写在系统条“记录”打开的我的回应记录里(2026-10-29 深夜) | 页头工具没有搬到折叠屏,见“还差什么” |
| 下拉收回详情(2026-10-29 深夜) | 跟手缩回原条,松手后按剩余比例走完 0.36 秒 | 跟着手指下移、变淡;过四分之一或快拉,从手指离开处往下 0.16 秒淡出,不缩回原条;不到就 0.20 秒弹回 | D 的“缩回原条”要一整套从详情到行的框;E 的详情开在左页、原条就在它旁边,下移变淡已读得出“放下” |
| 左滑知道了(2026-10-29 深夜) | 松手从当前位置收成“✓ 已知道” | 松手时行回到原位、就地换成已读的样子,提示字收掉 | E 的作答走自己的一套 give(),没有 D 那条从滑开处接着走的路径 |
| 桌面转屏 | 每条原地转成一列,0.36 秒 | 旧排布 0.16 秒淡出,新排布逐列淡入(错开 0.04 秒) | 转屏后一条横跨折痕的条变成上下两段,没有一条连续的路径可以“转” |
| 轻点已答的右半 | 未写 | 什么也不做,只有“撤回”起作用 | 回执就在原来按钮的位置,0.25 秒后再点同一处不该打开详情;看详情点左半 |
| 左页(2026-10-29 夜) | 3.1 的 A:左半写 Agent 为它做了什么 | 摊开这一轮的原料(E),A 留在表里作记录 | 1.6:A 的左半在 D 里都有,展开没有多出东西 |
| 半折静止时的线 | — | 150° 静止时线与份额带一样只长到 0.65,汇成一股的几笔离原话的行尾还差一截 | 跟份额带同一个缩放,读得出“还没全开”;不为线另设一条曲线 |
| 线程 | — | 同一来源、主题去掉 Re: 一类前缀后相同、又连着的几条才并成一行(×n) | 数据里没有线程号;隔开的同一线程会分成两行 |
| 桌面的灰行 | — | 进了 Agent 那边的与折掉的几行不画,只在页头计数 | 立着的半块屏放不下二十来行(4.1) |
性能上的三处改动:翻页逐帧只改 transform 与 opacity,前板的画在空闲时提前拍好(上文);每一拍只重画看得见的那一层(书本时不重画外屏,合上时不重画书本,等它下次出现时补上);一拍里所有的位置先一起量、再一起写(列表的位置、回执的起点和终点、详情的起点),不在写过 DOM 之后再读布局。改之前落地的第一帧在 2 倍像素下要 167 毫秒,之后约 33–50 毫秒(掉一帧)。
还差什么。
- 尺寸按 @3x 推算,20° / 120° 是常量,都待真机量;浏览器里的铰链是模拟的,真机上的手感、阻尼和系统转屏的时机都没有验证。
- 帐篷姿态没做(4.4)。
- 外屏页头没有 D 的稍后提醒计数;被稍后的事只在系统条“记录”打开的我的回应记录里看得到,到点提醒没做。
- 开合时只保证你正看着的那一张卡同高(3.3),它下面的几张在整理段里补位;外屏滚得不到 43 pt 时,书本按差额过渡,那张卡在右页上低一点。
- 桌面两半各自滚开时线不画,看不出这一列的组与卡还连着;没有做“滚动时线跟着两端伸缩”。
- 桌面上答过的列挪到最后,书本上原地:转屏时答过的几件换了位置(4.1)。挪列那一笔 0.40–0.76 秒,没有在真机上试过会不会让人以为卡被拿走了。
- 桌面的横滚用浏览器的
scroll-snap,没有 iOS 分页的手感;屏底的小点只是指示,不能点;只有鼠标没有横向滚轮时,横滚要靠 Shift + 滚轮(macOS 把它换成横向)或 Tab:焦点移到屏外那一列时条带跟过去(已测)。 - 窗口小只缩放整台,不改版式:1180 × 760 下一列在屏上约 120 px,字跟着缩到 0.63 倍(4.1)。
- 滚动是浏览器自己的,没有模拟 iOS 的回弹与大标题收起;半折时前板上的竖拖没有惯性。
- 桌面姿态的换轮只用通用的移位与进出场,没有“回执过分界”那一笔;桌面姿态里没有长按色带格。
- 外屏比内屏右页窄 9.5 pt,整理段里右半有 9.5 pt 的横向移动,第 3.3 节说的“x 距右缘不变”只近似成立。
- 视角取发布会的(看着外屏,前板翻开),没有做 Tech Talk 那种从铰链一侧看、两块板对称张开的镜头:浏览器里只有一个正对屏幕的视口,正对时右页不动才读得出“钉住”。
- 糊与暗的曲线、相机位置都是照视频估的:自由边最大模糊按 empirical.health 的 48 px @3x 取 16 pt,暗幕的深浅与时机是目测。透视取 2000 pt 而不是 1200–1600:1500 时 100–110° 的板上下伸出视口太多。
- 前板是两块屏的一张画,不是活的页面:翻页途中点不到左页;数据在翻页途中换轮时当帧重拍。半折静止时左页详情平铺在斜着的前板上,没有跟着倾斜。
- 英雄数字没有做跨页重新取景(上表)。
- 机身只照正面渲染图量:侧面、背面与相机凸台没画,正对屏幕的镜头里也看不到;合上时没有 Tech Talk 那种从侧面看的两块板。金属的光是一盏固定的灯算出来的,不随环境变。
- 屏的圆角改成与机身同心(1.6),合上时铰链一侧的机身角仍按 colors 取 24 pt,display-sizes 上更紧;没有对过真机。
- 左页的原话至多 500 字(数据里只留了这么多),没有链回原处;一组最多 6 / 4 行,其余并成“还有 N 条”,看全要点开。
- 线是固定的漏斗:一组 6 行时汇流处挤,没有按内容避让。
- story 数据每轮只有三条上下,左页多是“本轮没有新输入”的组,摊开的效果在 composite 上才看得出。
- 机身的键、缺口与天线断点是照渲染图的位置画的,没有对过真机;键不能按。
- 装上机身后,CPU ×4 下展开每次比 main 约多一个长帧,落在 145°–170°,没查出是哪一层(第 6 节)。2026-10-29 夜在负载 5–7 下与 #43 交替测,展开两边相当,这一条没有复现;也没有查。
- 摊开以后,答一件、落地与桌面 → 书本在 CPU ×4 下最长一帧的中位比 #43 多 15–17 毫秒(第 6 节),没有再压:左页多了组的盒子、原话与 158 格细格。
- 答一件在 CPU ×4 下第一帧比 #45 长约 17 毫秒:下面那几行也要量、要补位(第 6 节),没有再压。
- 钉住页头以后,展开与合上在 CPU ×4 下每次比 #44 多一个 33 毫秒的帧:展开在起手第一帧,合上在退出跨页之后(第 6 节)。脚本与排版和 #44 相同,多出的在绘制或合成,没有查实。
6 验证
答完一件以后(2026-10-30,分支 codex/duo-answered,基于 main 1fb0a2d)。本机无头 Chrome,截图与帧率加 --gpu,负载均值 6–9。静态服务同上(与 prototype/serve.py 相同的设置,换了端口)。
make check、make test两组命令通过(用主仓库的.venv)。scripts/check-ix.mjs --only duo,home,c,composite 与 story:duo 各 49 / 49,home 各 23 / 23,c 各 16 / 16。duo 比 #45 多五项,改了三项:- 改:作答——右半收成回执(标题一行 + 回执),行的上沿不动,份数少这一件;作答后——两半同高,且与 D 的已答卡同高(检查先打开 D 的同一帧、答同一件量出 D 的卡高),左页那组折成一行、不再连线,下面那一行上移的距离等于这一行收走的高;撤回——两半回到原高,下面那一行回到原处。原来的“卡高与行位不变”“那组输入在行底”两项按 3.4 的新做法换掉。
- 新:桌面列宽相等(191 pt)、上下两半同一组列同一 x、组与卡各自上沿对齐、三列一屏;桌面答一件——那一列不变窄、回执高小于宽、那组折成一行贴在列顶、答过的列在最后、页头高不变、列多于三列时能横滚且答过的那列在屏外、小点数等于列数且前三个是墨色;横滚按列吸附(滚到 150 停在 199,滚到 40 回到 0,两半一起移);桌面撤回——每列的件与高逐项回到作答前,条带从 199 跟回 0;1180 × 760——缩放比 1440 × 900 小,列仍是 191 pt,至少两列整列可见,答一件后同样。
scripts/shoot-forms.mjs --only duo,d,c,all --gpu --check all-sync,两套数据:无页面报错、无控制台错误,合页逐节同步无失败。- D 与 C 的画面:main 与本分支的 D、C(composite 与 story,f4、f22、f26,隐藏切换栏)截图逐字节相同。E 的这几帧(没有作答,书本)也逐字节相同,所以
site/previews/duo.webp不用重截。d-view.mjs、d.mjs、d.css、d-motion.mjs、c.mjs、inflow.mjs、shared.mjs、scenario.mjs、engine.mjs、respond.mjs与演示数据都没改。 - 量出来的(pt):composite f26 主水管选“整根挖开换”,D 的已答卡 83,E 书本两半 83 / 83(作答前 233 / 233)。桌面:1440 × 900 下整台缩放 0.767、1180 × 760 下 0.627,两种窗口都是一列 191、各列左沿 12 / 211 / 410 / 609 / 808;答一件后那一列的回执 110 高(宽 191),那组 70 高、贴在列顶;钉住区作答前后都是 166。书本的钉住区两套数据 f04、f10、f22、f26 连答四件都是 167。
- 手势(本机 scratchpad 里的脚本):在下半横着触摸滑 110 px,条带停在 199,滑回停在 0;竖着滑只滚那一半;横向滚轮落在上半的组上也滚条带;Tab 到最后一列的“撤回”,条带跟过去。
- 讲一遍的答一件(书本)逐帧看了 0.10–0.75 秒;作答后的下一轮落地,回执照常斜穿折痕。
npm run site:build通过,从dist/打开的 E:书本答一件两半 83 / 83,桌面四列都是 191,无报错。合页导出node scripts/export-all.mjs --out .local/export/test.html以file://打开,E 那一节同样,无报错。- 静帧对照:桌面答 0 / 1 / 2 件(1440 × 900)、桌面答 1 件(1180 × 760)、书本答 0 / 1 / 2 件、合上答 1 件,composite f26、composite f22、story f04 各一行,拼成一张,只在本机 scratchpad。
- 帧率(composite,1440 × 900、2 倍像素,CPU ×4;本分支与 main 交替各跑 6 次,取中位,括号里是 6 次最长一帧的范围;答一件、落地、详情用 f22,其余同上一轮。答一件这次录 0.9 秒、桌面 1.2 秒,与之前几轮的表不能直接比):
| 一拍 | 本分支 | main(#45),同一时段 |
|---|---|---|
| 书本答一件(本分支 0.72 秒,main 0.40) | 50.0 fps,2 长帧,最长 100 毫秒(100–183) | 50.5 fps,2 长帧,最长 84 毫秒(83–150) |
| 桌面答一件(本分支 0.92 秒,main 0.40) | 57.5 fps,1 长帧,最长 67 毫秒(67–67) | 57.9 fps,1 长帧,最长 50 毫秒(50–67) |
| 合上答一件(D 的 3a) | 57.8 fps,1 长帧,最长 50 毫秒(33–50) | 58.3 fps,1 长帧,最长 42 毫秒(33–50) |
| 下一轮落地(1.0 秒) | 50.7 fps,3 长帧,最长 150 毫秒(150–633) | 51.0 fps,2 长帧,最长 150 毫秒(133–300) |
| 展开(跟手 1.5 秒 + 整理 0.40 秒) | 57.4 fps,3 长帧,最长 83 毫秒(83–167) | 57.4 fps,3 长帧,最长 83 毫秒(83–117) |
| 合上(倒放 0.28 秒 + 跟手 1.1 秒) | 57.5 fps,4 长帧,最长 42 毫秒(33–50) | 57.8 fps,4 长帧,最长 33 毫秒(33–83) |
| 左页详情(D 的,0.48 秒) | 53.8 fps,1 长帧,最长 100 毫秒(83–233) | 54.4 fps,1 长帧,最长 83 毫秒(83–333) |
| 左页一组的原文(0.48 秒) | 57.5 fps,1 长帧,最长 50 毫秒(33–83) | 57.5 fps,1 长帧,最长 50 毫秒(50–67) |
| 桌面 → 书本(0.36 秒) | 54.3 fps,1 长帧,最长 83 毫秒(67–83) | 54.3 fps,1 长帧,最长 83 毫秒(67–133) |
| 书本滚动(每帧 12 pt,50 帧) | 60 fps,0 长帧,最长 17 毫秒(17–33) | 60 fps,0 长帧,最长 17 毫秒(17–33) |
| 外屏滚动(同上) | 60 fps,0 长帧,最长 17 毫秒(17–17) | 60 fps,0 长帧,最长 17 毫秒(17–17) |
| 外屏滚过 380 pt 后展开 | 58.0 fps,2.5 长帧,最长 67 毫秒(67–67) | 58.3 fps,2 长帧,最长 67 毫秒(67–67) |
答一件变长了(收起之后下面的还要一次补位,桌面还要挪列),帧率与 main 相当。逐帧记下书本答一件的长帧(两边各 6 次):两边都是第一帧(本分支 67–117 毫秒,main 50–100)和 0.75–0.82 秒的一帧(重画 0.7 秒后空闲时拍前板的画,原来就有);本分支第一帧长约 17 毫秒,是下面那几行也要量、要排进 FLIP。落地 633 毫秒那一次是本分支第一遍冷启动,其余五次 150 毫秒。CPU ×1 没有量。
没有验证的:真机;Safari(横向的 scroll-snap 里套两个竖向滚动容器、分轴的 overscroll-behavior);story 数据的帧率;触屏下原生横滚的惯性与吸附手感(无头 Chrome 的合成手势没有惯性)。
钉住页头以后(2026-10-29 深夜,分支 codex/duo-scroll,基于 main dc4028b)。本机无头 Chrome,截图与帧率加 --gpu,负载均值 7–8。静态服务用与 prototype/serve.py 相同的设置(不缓存、监听队列 256),只换了端口;Python 自带 http.server 的默认队列是 5,合页那一遍会重置模块请求。
make check、make test两组命令通过(worktree 里没有.venv,用主仓库的.venv跑同样的命令)。scripts/check-ix.mjs --only duo,home,c,composite 与 story:duo 各 44 / 44,home 各 23 / 23,c 各 16 / 16。duo 比 #44 多八项:- 书本滚到一半:两页页头、份额带、账的 y 一像素不变,第一组和它那张卡一起上移、仍在同一行,分界的不透明度从 0 到 1。
- 滚轮落在钉住的页头上:滚的是列表,整页不动。
- 外屏页头钉住:滚 380 pt 时页头不动,第一张卡上移 380 pt,分界显出。
- 外屏滚过以后翻开:正看着的那张卡在右页上同高(差 ≤ 1.5 pt),书本不在顶上;再合上,外屏回到原来的位置。
- 折痕的折叠区:点三处(−8、0、+8 px,避开把手;浏览器把 −8 那一下挪到了旁边那一组,这条规则就是为它加的),什么都不变。
- 半折 150°:画上的条目层平移 −200,页头不动;在斜着的左页上竖拖,书本跟着滚,画跟着移。
- 桌面:composite 先在减少动态下找第一轮下半溢出的 f03 再打开。页头不动,下半滚 40 pt,列不横移、组与卡同列(误差 0),滚开时线与小圈淡出、分界显出,转回桌面两半回到顶上、线回来。story 每轮都放得下,只核对列对齐和线都在。
- 左滑在办的一行是知道了;详情短拉弹回、长拉收起;系统条的“记录”打开我的回应记录。
旧的 36 项一项没改。写新项时找出的两处问题已经修掉:桌面上下的列错开约 2 pt(改成
minmax(0, …));浏览器会把落在折叠区的点按挪到旁边那一组。scripts/shoot-forms.mjs --only duo,d,c,all --gpu --check all-sync,两套数据:无页面报错、无控制台错误,合页逐节同步无失败。D 与 C 的画面:main 与本分支的 D、C(composite 与 story,f4、f22、f26,隐藏切换栏)截图逐字节相同。
d-view.mjs、d.mjs、d.css、d-motion.mjs、c.mjs、inflow.mjs、shared.mjs、scenario.mjs、engine.mjs、respond.mjs与演示数据都没改。E 的默认帧:页头、份额带、账与第一张卡的位置与 main 相同(逐项量过,单位 pt)。页头不再在滚动层里,栅格化时文字落点差 1 px;系统条的“记录”变成了按钮。所以
site/previews/duo.webp按 development.md 的参数重截了。npm run site:build通过,从dist/打开的 E 无报错,页头钉住、书本可滚。合页导出node scripts/export-all.mjs --out .local/export/test.html以file://打开,E 那一节无报错、页头钉住、书本可滚。按操作顺序另走了一遍(本机 scratchpad 里的脚本):
[]、F、退出跨页再回来,书本与外屏的滚动都不变;减少动态时开合照样接上滚动位置。- 换轮时你正看着的那条留在原来的 y。
- Tab 的顺序:外屏是页头的工具、卡、卡上的按钮;书本是每一行先左半、后右半的按钮。每一下焦点都在列表的可见范围里,没有落到页头底下。
- 桌面的一组原文长满上半,下半的按钮不被盖住。
静帧对照:合上滚到顶 / 滚到一半、书本滚到顶 / 滚到一半 / 开着一组原文、桌面下半滚到一半、半折 150° 滚到一半,composite f26 与 story f04 各一行(composite 的桌面取 f04,f26 两半都放得下),拼成一张,只在本机 scratchpad。
帧率(composite,1440 × 900、2 倍像素,CPU ×4;本分支与 main 交替各跑 6 次,取中位,括号里是 6 次最长一帧的范围;展开、合上、桌面、滚动用 f26,答一件、落地、详情用 f22):
| 一拍 | 本分支 | main(#44),同一时段 |
|---|---|---|
| 展开(跟手 1.5 秒 + 整理 0.40 秒) | 57.4 fps,3 长帧,最长 83 毫秒(83–167) | 57.8 fps,2 长帧,最长 83 毫秒(67–83) |
| 合上(倒放 0.28 秒 + 跟手 1.1 秒) | 57.8 fps,3 长帧,最长 50 毫秒(33–133) | 58.9 fps,2 长帧,最长 33 毫秒(33–34) |
| 答一件(0.40 秒) | 54.3 fps,1 长帧,最长 83 毫秒(83–117) | 54.3 fps,1 长帧,最长 83 毫秒(67–83) |
| 下一轮落地(1.0 秒) | 51.4 fps,2 长帧,最长 150 毫秒(150–233) | 51.4 fps,2 长帧,最长 150 毫秒(133–167) |
| 左页详情(D 的,0.48 秒) | 54.4 fps,1 长帧,最长 92 毫秒(83–150) | 55.0 fps,1 长帧,最长 83 毫秒(67–83) |
| 左页一组的原文(0.48 秒) | 57.5 fps,1 长帧,最长 50 毫秒(33–50) | 57.5 fps,1 长帧,最长 50 毫秒(33–50) |
| 桌面 → 书本(0.36 秒) | 54.3 fps,1 长帧,最长 83 毫秒(67–83) | 55.6 fps,1 长帧,最长 67 毫秒(67–100) |
| 书本滚动(每帧 12 pt,50 帧) | 60 fps,0 长帧,最长 17 毫秒(17–33) | 60 fps,0 长帧,最长 17 毫秒(17–17) |
| 外屏滚动(同上) | 60 fps,0 长帧,最长 17 毫秒(17–33) | 60 fps,0 长帧,最长 17 毫秒(17–17) |
| 外屏滚过 380 pt 后展开 | 58.3 fps,2 长帧,最长 67 毫秒(50–67);接着读 | 58.7 fps,1.5 长帧,最长 67 毫秒(50–150);从顶上读 |
答一件、落地、详情、原文、两种滚动与 main 相当:钉住区下面的滚动只改 scrollTop、一个类名与前板副本的一个 transform,滚动时没有长帧。展开与合上每次多一个 33 毫秒的帧。逐帧记下角度(各 3 次):
- 展开多出的那一帧是起手第一帧,每次都有;120° 整理那一帧两边都在(67–83 毫秒)。
- 合上多出的那一帧落在 105° 退出跨页之后的 95° 或 76°。
- 这两拍第一帧的脚本与排版和 main 相同:起手是脚本 1–3 毫秒加样式与排版 6–9 毫秒(main 1–3 加 5–11);退出跨页是 36–43 加 3–4(main 34–43 加 3–4)。所以多出的是绘制、栅格化或合成。前板上的画现在分两层(钉住的页头与滚动的条目),合上时外屏的页头也在滚动容器外,可能是这里多出来的。藏掉分界的伪元素以后,这一帧时有时无,没有查实是哪一层。CPU ×1 没有量。
没有验证的:真机;Safari(钉住区与 overscroll-behavior 在 iOS Safari 上的表现、前板副本里 overflow: hidden 的裁切);story 数据的帧率;触屏下原生的惯性滚动(无头 Chrome 的触摸事件不产生惯性)。
摊开以后(2026-10-29 夜,分支 codex/duo-unfolded,基于 main df44a0a)。本机无头 Chrome,截图与帧率加 --gpu,负载均值 5–7。
make check、make test通过。scripts/check-ix.mjs --only duo,home,c,composite 与 story:duo 各 36 / 36,home 各 23 / 23,c 各 16 / 16。duo 比 #43 多五项:左页摊开(本轮每条输入记一次,进右页 + 进 Agent 那边 + 折掉 = 本轮条数;每组与它那张卡同一行、同一高;进了输入的组各有一条线、线的两端在折痕两侧);作答后那一组折成一行(比原来矮、底边与行底齐)、不再连线;线随铰链长出(108° 为 0;144° 在 0.2–0.8 之间,看得见的那张清楚副本上的scaleX与它相同,模糊副本上没有线;180° 为 1);轻点一行灰的,原文开在左页、条数与那一行的 ×n 相同、右页的卡一像素不动,从一组原文点“为什么在这里”换成 D 的详情;圆角同心(合上时外屏四角、书本时内屏四角、桌面四角,计算出的圆角都等于机身的角减去那一侧的边框,误差 0)。改过的三项:轻点左半,开的是那一组的原文;作答后右半的高与上沿不变(原来是两半都不变);撤回后那一组重新摊开、线回来。scripts/shoot-forms.mjs --only duo,d,c,all --gpu --check all-sync,两套数据:无页面报错、无控制台错误,合页逐节同步无失败。- D 与 C 的画面:main 与本分支的 D、C(composite 与 story,f4、f22、f26,隐藏切换栏)截图逐字节相同;C 每一帧的去向计数(
grainPlan的结果)也逐项相同。d-view.mjs、d.mjs、d.css、d-motion.mjs,shared.mjs、scenario.mjs、engine.mjs、respond.mjs与演示数据都没改;c.mjs只把去向规则改成从inflow.mjs取。 - E 的画面变了:
site/previews/duo.webp按 development.md 的参数重截。 npm run site:build通过,dist/里有inflow.mjs,从dist/打开的 E 无报错、页头计数相同。合页导出node scripts/export-all.mjs --out .local/export/test.html以file://打开,E 那一节里左页的组都在,无报错。- 静帧对照:合上、54°、108°、144°、半折 150°、书本(作答前、作答后)、桌面,两套数据各一行,拼成一张;合上与书本的四个角放大,main 与本分支上下对照,描出机身与屏的圆弧和圆心另拼一张。都只在本机 scratchpad。
- 帧率(composite,1440 × 900、2 倍像素,CPU ×4;本分支与 main 交替各跑 6 次,取中位,括号里是 6 次的最长一帧的范围;展开、合上、桌面用 f26,答一件、落地、详情用 f22):
| 一拍 | 本分支 | main(#43),同一时段 |
|---|---|---|
| 展开(跟手 1.5 秒 + 整理 0.40 秒) | 58.3 fps,2 长帧,最长 67 毫秒(50–83) | 58.7 fps,2 长帧,最长 67 毫秒(33–83) |
| 合上(倒放 0.28 秒 + 跟手 1.1 秒) | 59.1 fps,1.5 长帧,最长 33 毫秒(33–50) | 59.4 fps,1 长帧,最长 33 毫秒(17–50) |
| 答一件(0.40 秒) | 55.0 fps,1 长帧,最长 75 毫秒(67–83) | 56.4 fps,1 长帧,最长 58 毫秒(33–117) |
| 下一轮落地(1.0 秒) | 53.2 fps,2 长帧,最长 117 毫秒(100–150) | 54.6 fps,2 长帧,最长 100 毫秒(67–300) |
| 左页详情(D 的,0.48 秒) | 56.2 fps,1 长帧,最长 66 毫秒(50–100) | 57.5 fps,1 长帧,最长 50 毫秒(33–250) |
| 左页一组的原文(0.48 秒) | 58.8 fps,1 长帧,最长 33 毫秒(17–50) | — |
| 桌面 → 书本(0.36 秒) | 55.7 fps,1 长帧,最长 67 毫秒(50–83) | 57.1 fps,1 长帧,最长 50 毫秒(33–100) |
展开与合上和 #43 相当:线、账与小圈逐帧只写 transform 与 opacity,模糊副本上不画线,翻页途中没有多出长帧。答一件、落地、详情与转屏的帧率低 1–1.5 fps,最长一帧的中位多 15–17 毫秒,但 main 的最坏一次(250–300 毫秒)更长;多出的是重画左页那组的盒子与原话(作答时折起、换轮时换掉)。CPU ×1 没有量。
没有验证的:真机;Safari(线是 SVG,前板 3D 里的清楚副本上带着它);story 数据的帧率;真机上系统条的安全区与这里时间离上沿的 52 pt 是否一致(照片上约 35 pt,这里多留了灵动岛那一格)。
装上机身以后(2026-10-29 晚,分支 codex/duo-body,基于 main c2fd466)。本机无头 Chrome,截图与帧率加 --gpu。这一轮机器上同时跑着别的 Agent,负载均值 15–50,比 #42 那次的 7–13 高得多,帧率的绝对值都偏低,所以帧率是本分支与 main 交替跑、同一时段比的。
make check、make test通过。scripts/check-ix.mjs --only duo,home,composite 与 story:duo 各 31 / 31,home 各 23 / 23。duo 比 #42 多两项。一项是三种姿态都有机身:合上是前盖连外屏,四颗键露出上沿与右缘;书本是宽机身,折痕处两块铰链盖,开过一次详情再看也还在;桌面也装在机身里。另一项是翻页途中,27°、54°、90°、144° 四个角度上手里那半台的机身外框逐位相同;前板的自由边 90° 时最宽,54° 在右、144° 在左都更窄;亮面与暗面随角度变。story 有一遍在负载 44 时“拖把手”那项读到 149°(松手后的吸附还没走完),重跑通过。scripts/shoot-forms.mjs --only duo,all --gpu --check all-sync,两套数据:无页面报错、无控制台错误,合页逐节同步无失败。- D 的画面:main 与本分支的 D(composite f26、story f04,隐藏切换栏)截图逐字节相同;
d-view.mjs、d.mjs、d.css、d-motion.mjs,shared.mjs、scenario.mjs、engine.mjs、respond.mjs与演示数据都没改。 - E 的画面变了:
site/previews/duo.webp按 development.md 的参数重截。 - 合页导出:
node scripts/export-all.mjs导出的单文件以file://打开,E 那一节里机身在(四副壳、八颗键、自由边),无报错。机身没用图片,样式里没有url()。 npm run site:build通过,dist/里的 E 带机身、无报错。- 静帧对照:0°、27°、54°、81°、108°、144°、180° 与 Tech Talk 111464 的对应帧拼成一张;#42 与本分支同角度(加半折、桌面)另拼一张。都只在本机 scratchpad。
- 帧率(composite,1440 × 900、2 倍像素,CPU ×4;本分支与 main 交替各跑 6 次,取中位;“长帧”指超过 25 毫秒的帧间隔;“最长”是每次最长一帧的中位):
| 一拍 | 本分支 | main(#42),同一时段 | #42 当时记的(负载 7–13) |
|---|---|---|---|
| 展开(跟手 1.5 秒 + 整理 0.40 秒) | 48.9 fps,5.5 长帧,最长 225 毫秒 | 55.6 fps,4 长帧,最长 117 毫秒 | 57.8 fps,3 长帧,各次最长 83 / 50 / 67 毫秒 |
| 合上(倒放 0.28 秒 + 跟手 1.1 秒) | 57.9 fps,2.5 长帧,最长 50 毫秒 | 57.0 fps,3.5 长帧,最长 50 毫秒 | 58.8 fps,2 长帧,各次最长 33 毫秒 |
| 答一件(0.40 秒) | 47.1 fps,1 长帧,最长 142 毫秒 | 50.6 fps,1 长帧,最长 125 毫秒 | 57.1 fps,1 长帧,最长 50–67 毫秒 |
| 下一轮落地(1.0 秒) | 42.2 fps,4 长帧,最长 325 毫秒 | 43.5 fps,4 长帧,最长 292 毫秒 | 50 fps,2 长帧,各次最长 200 / 83 / 83 毫秒 |
| 左页详情(0.48 秒) | 48.9 fps,2 长帧,最长 142 毫秒 | 48.0 fps,2 长帧,最长 150 毫秒 | 57.4 fps,1 长帧,最长 50–83 毫秒 |
| 桌面 → 书本(0.36 秒) | 54.3 fps,1.5 长帧,最长 75 毫秒 | 52.8 fps,2 长帧,最长 83 毫秒 | 57.1 fps,1 长帧,最长 33–50 毫秒 |
同一时段里,合上、答一件、落地、详情、桌面与 main 相当(前后几次交替测,谁高谁低互有)。展开每次约多一个长帧:逐帧记下长帧落在哪个角度,两边都有 120° 整理那一帧(83–150 毫秒);起手那一帧两边都时有时无;本分支多出的落在 145°–170°(份额带长出、前板贴近平放的那一段),每次一两帧、各 33–50 毫秒,没查出是哪一层。为了到这里改过四处,都没加模糊:换壳只换 opacity,不重画;外屏当右页的那 9 pt 原先切 clip-path,起手那帧要重画整块外屏,改成只淡入淡出的盖板;前板收起时透明而不是 visibility: hidden,翻开那一帧不用现画边框与副本;两面的高光从两面宽的渐变层缩成一条 226 pt 的带(背面转进视野那一帧要栅格化的少了)。藏掉圆角的小平面再测,长帧不变,所以没减它们。CPU ×1 这次没有重量。
没有验证的:真机;Safari(3D 变换叠多圈 inset 阴影与模糊);story 数据的帧率;帧率是在高负载下测的,负载低时应再量一遍。
重做开合时(#42)的验证。 2026-10-29 重做开合以后在本机无头 Chrome 里重跑(截图与帧率加 --gpu,负载均值 7–13),基于 main a0f9b10。第一版(2026-10-28)的验证结论见本节末尾。
make check通过。scripts/check-ix.mjs --only duo,composite 与 story:各 29 / 29。比第一版多一项:原来的“?hinge=0.4定格”换成两项,一项是?hinge=0.4定格在 72°,前板绕折痕转的角度(从它算出的 transform 读回)与铰链一致,右页仍是外屏、写“件”;另一项是 108° 时左页比 144° 更糊(显示的模糊副本更多、清楚的一段更窄)、更暗,180° 平放时前板收起。拖把手那一项改成读前板转角:拖到 54° 时前板也在 54°,松手落回合上、落到书本后前板都收起。其余 27 项同第一版(见下)。scripts/check-ix.mjs --only home,两套数据:各 23 / 23。scripts/shoot-forms.mjs --only duo,all --gpu --check all-sync,两套数据:无页面报错、无控制台错误,合页逐节同步无失败。- D 的画面:隐藏方案切换栏后,main 与本分支的 D(composite f26、story f04)截图逐字节相同;
d-view.mjs、d.mjs、d.css、d-motion.mjs都没改。 - E 平放书本的画面(预览图的参数:composite f26 与 story f04,隐藏切换栏)与 main 逐字节相同,所以当时
site/previews/duo.webp不用重截(装上机身后已重截,见上)。前板升起时内屏只亮右页,落平时整屏亮起、发丝边与改之前相同。 - 静帧对照:
?hinge=0 / 0.15 / 0.3 / 0.45 / 0.6 / 0.8 / 1(0°、27°、54°、81°、108°、144°、180°)两套数据各一张;跟手展开、合上与讲一遍另录了屏,逐帧看过(只在本机 scratchpad)。 - 帧率(composite,1440 × 900、2 倍像素,按掉帧数取中位;“长帧”指超过 25 毫秒的帧间隔):
| 一拍 | CPU ×1(5 次) | CPU ×4(3 次) |
|---|---|---|
| 展开(跟手 1.5 秒 + 整理 0.40 秒) | 60 fps,0 长帧,最长 17 毫秒 | 57.8 fps,3 长帧,各次最长 83 / 50 / 67 毫秒 |
| 合上(倒放 0.28 秒 + 跟手 1.1 秒) | 60 fps,0 长帧,最长 17 毫秒 | 58.8 fps,2 长帧,各次最长 33 / 33 / 33 毫秒 |
| 答一件(0.40 秒) | 60 fps,0 长帧 | 57.1 fps,1 长帧,各次最长 50–67 毫秒 |
| 下一轮落地(1.0 秒) | 59.3 fps,1 长帧,最长 33 毫秒 | 50 fps,2 长帧,各次最长 200 / 83 / 83 毫秒 |
| 左页详情(0.48 秒) | 60 fps,0 长帧 | 57.4 fps,1 长帧,各次最长 50–83 毫秒 |
| 桌面 → 书本(0.36 秒) | 60 fps,0 长帧 | 57.1 fps,1 长帧,各次最长 33–50 毫秒 |
展开与合上在 CPU ×4 下另外各连跑 6 次,与 main 并排:展开每次最长 33–83 毫秒(main 50–83),合上每次 33 毫秒(main 33)。调参途中合上出现过一次 117 毫秒,之后 15 次没有再出现。展开的长帧在起手与 120° 整理开始的那一帧,之后只走合成层;CPU ×4 下展开没有超过 100 毫秒的帧。落地的长帧是原有的:作答后静置再落地,main 与本分支各跑 5 次,最长 83–183 与 67–217 毫秒,同一量级,不是翻页带来的。前板的画在空闲时拍,4 倍降速下建 DOM 加排版约 30–45 毫秒,静置时没有测到超过 50 毫秒的长任务;作答后 1.5 秒起手,第一帧 17–50 毫秒;作答后 0.7 秒就起手(画还没拍好),第一帧约 67 毫秒。
没有验证的:真机(尺寸、阈值、铰链手感与系统转屏时机);Safari(3D 变换叠模糊与遮罩在 Safari 上的表现与性能尤其要看);story 数据的帧率;npm run site:build(本 worktree 没装依赖,这次没改体验站)。
第一版(2026-10-28)的验证,当时同时有六七个 Agent 在跑浏览器,负载均值 30–55:scripts/check-ix.mjs 全部页面 × 两套数据,E 另跑的 28 项每一遍两套都过。28 项是:带帧号打开不演开场;长按看概要、点空白收起、移动超过 10 px 取消长按、长按色带格把那一件的两半描出来;轻点左半详情长满左页(不越过折痕)、详情开着时右半的按钮仍可点、Esc 与点空白收回、详情开着时作答;作答后右半就地收成回执、条高不变、份数少这一件,0.25 秒后再点同一处不再作答,撤回复原,焦点落在“撤回”;Tab 到单行条或行出概要、回车展开、Esc 回原处;三个姿态按钮(外屏“件”、书本与桌面“份”)、F、[ ] 与 165° 半折、105° 退出跨页、拖把手跟手并按 90° 吸附;连播中长按停住、换轮时概要收起;回执跨两轮跟着条目走;?hinge=0.4 定格;减少动态直达终态;直接打开演一遍、任意键跳到结尾。机器忙时动画起播会晚,左页详情一项在读状态前等动画落定(至多再等 1 秒)。帧率当时是展开 ×4 最长 117 毫秒、落地 ×4 最长 133 毫秒,其余同上表的量级;npm run site:build 通过。
7 草图(记录阶段)
静态 HTML,只在本机 scratchpad,不入库。数据是 composite f26 实时回放;样式复用 variants/d.css,Duo 版式另写一份 duo.css。用 headless Chrome 按 @3x 实像素截图:外屏 1398 × 2034 px,内屏 2853 × 2007 px,桌面 2007 × 2853 px。
| 图 | 内容 |
|---|---|
| ① 合上 | 外屏上的 D:5 件等你回应,系统条在右缘 |
| ② 展开 40%(铰链进度 0.4) | 右页仍是外屏原样;每张卡的左半像窗一样开到四成,先露出档位、变化、份额;右卡上 Agent 的几行朝折痕淡出 |
| ③ 整理 0.14 秒 | 右半压紧到一半,左右同一行;数字还是“件”;累计栏在右页底部退场 |
| ④ 全开书本 | 38 份 / 25 份;三条大条加单行条;份额带分界在折痕上 |
| ⑤ 答一件 | 主水管选“免挖穿新管”:右半成回执,条高不变,14 份,24 格空心 |
| ⑥ 桌面 | 上半 Agent 的半,下半你的半,三列,加一行单行 |
对照页(草图 + 6 张参考图):
- 页面:
/private/tmp/claude-501/-Users-nobody1-orca-fold-skill/218a8ef9-ef76-4968-83de-e3eca8b3cce8/scratchpad/duo/sheet/index.html - 整页截图:
/private/tmp/claude-501/-Users-nobody1-orca-fold-skill/218a8ef9-ef76-4968-83de-e3eca8b3cce8/scratchpad/duo/sheet/duo-sheet.png
重画:
- 在
scratchpad/duo/site/起本地静态服务(prototype是指向工作区原型目录的软链接)。 - 打开
sketch/duo.html?posture=closed|split&p=0.4|settle&t=0.14|book|table,可另加&frame=f22、&data=story、&answer=<item>:<按钮文字>。 - 截图脚本是
tools/shots.sh <帧> <前缀> <倍数>。
scratchpad 是临时目录,草图丢了可以照本文重画,结构与数字以本文为准。