切换外观
原版树图 · 动画记录:参考、数据、故事
状态:2026-10-20 按第三轮的六步流程(reference → data → story → motion → direct → ship)重做原版树图的动画与交互。第 1–3 节在任何东西动之前写下,定下一个点、一个英雄数字与三拍;第 4–6 节是照它做完之后的实现、出入与验证。原型探索,不是规范。树图怎样承载信息、承载哪类回应仍记在 spatial.md;这次改动的动画与交互以本文为准。范本是 home-motion.md(换轮三拍、答一件少一件、回执跟着条目走)与 watch-motion.md(阅读工具的概念层只占两成、只在节拍上动),两层法则与交互标准见 CONCEPTS.md。下面的数都由页面用同一份 scenario.mjs 回放现算,规则见 2.1;推算脚本只留在本机,不入库。
树图与两个概念动画有两处根本不同:它是阅读工具,坐在电脑前一格一格读,静止是默认;它能作答,工作评审的格子里直接有“批准 / 要求修改”。所以概念层约两成、只在两种节拍上动:换轮(←/→、连播、合页顶栏选帧)与作答。“丝滑”来自同一件事的格子在换轮、作答、展开之间位置连续,不靠粒子。
1 参考(reference)
1.1 树图现在怎么变
读 app.mjs、styles.css 与 variants/spatial-kit.mjs 的 createFlipBoard 所见(原来的 motion.mjs 是更早的克隆幽灵实现,已经没有页面在用)。格子按 data-id 保留 DOM,FLIP 从旧矩形走到新矩形,一律 320 ms,cubic-bezier(.2,.75,.25,1):
| 时刻 | 现在怎么动 | 断在哪 |
|---|---|---|
| 换轮 | 留下的格子平移并缩放色块(flip-shape),正文只平移、用 clip-path 裁到色块大小;内容改了的旧正文 0.6 前淡出、新正文 0.18 后淡入;新格子从目标 60% 大小处淡入,延迟 0.18;撤下的原地缩到 55% 淡出;合并的来源飞进目标;旁白、日期、计数一帧硬切;左栏与右栏整块重画,旁白区 180 ms 浮一下 | 全部挤在 0–0.32 秒一起动,没有先后。composite 每轮中位有 11 格在变(见 2.4),眼睛不知道先看哪;数字与格子同一刻变,看不出“因为什么少了一件”;clip-path 的动画留在主线程 |
| 作答(格子里批准 / 要求修改,详情里其余) | 整页重画:格子换成回执一行(og-settle 260 ms 浮入),右栏重画,屏幕底部弹 4.2 秒 toast“你批准了 · 已保存” | toast 把视线拉到屏幕底;格子的标题与摘要在重画里闪一下 |
| 下一轮整理之后 | 回执按条目与问题 id 查(answers.find(item, cursor, ask.id)),Agent 一换问题就取不到;另弹 toast“Agent 已在 12:00 的整理中收到你的 1 条回应” | f22 批准 #13572,f23 这一格换成“三个旧 PR 推到哪天”,回执凭空消失,只剩一句会消失的 toast |
| 详情 | 点格子,右栏整块换成这一件,内容 180 ms 浮一下 | 与被点的格子没有几何联系:格子在中间,详情在右边整块替换,收回也不回到格子;与拨盘、脉冲“沿自己的几何弹出”不是同一标准 |
| 悬停 | 只有被裁切或很小的格子 320 ms 后弹一张自写的卡(完整标题与问题) | 大格子没有概要;与 ix 的概要卡、0.15 秒宽限、停住播放都不是同一套 |
| 连播 | 每 3.2 秒一轮(320 ms 过渡 + 2.9 秒读),setInterval | 悬停、展开都不停;作答也不停,刚按下的回执可能下一秒就被换轮冲走 |
1.2 从 D 与手表搬什么,不搬什么
| 要素 | 出处 | 树图 |
|---|---|---|
| 先换字,0.4 秒后才动第一个数;列表按“离场 → 移位 → 进场”依次走 | D BEATS.turn | 搬。切点换日期、旁白、整理依据、左栏与右栏的字;0.40 秒英雄数字翻一格;离场 0.40、移位 0.48、进场 0.60,进场错开 40 ms、最多 8 个 |
| 你做过的先确认 | D:回执 0–0.30 秒填实,列表其余不动 | 搬。上一轮按下的回执 0–0.30 秒填实、小字在切点换成“已收到 · Agent 在 12:00 的整理中读到”,格子都还没动 |
| 答一件,少一件:卡原地收成回执,下一轮整理读到了才带着让出的份额沉下去 | D 3.4 | 搬,落在面积上:格子不动、份额不变,只是提问与按钮收成回执、数字减一;份额要等下一轮整理才变,那时它和别的格子一起按真实的下一帧缩放、移位 |
| 回执跟着条目走,不随版本消失 | D 4.4 | 搬。同一个问题重登记仍是回执;问题关上,回执留成格子里的一行小字;换成另一个问题,格子写新问题,下面一行“你批准了上一版 · 已收到” |
| 数字只在节拍上动,差 1 滚一格、差 2 以上一帧硬翻 | D roll | 搬到英雄数字;格子上的“N 份”在移位落定(0.84 秒)时翻,不交叉淡化 |
| 份额变化沿自身几何长缩 | 手表:64 格逐格换主人;拨盘:扇区沿弧长缩 | 落在矩形上:色块从旧矩形缩放到新矩形,字不随之拉伸(反向缩放保持 1:1),留在色块左上角、被色块裁住;只动 transform 与 opacity |
| 概念层两成,只在节拍上动,静止为零 | 手表:本轮输入化成灰点落向表圈 | 搬,但只画落进格子的。本轮采用且归到板上某一格的输入化成灰点,从左栏流向中间、落进那一格,一条一粒、最多 40 粒;没采用的不画;只用灰,只在换轮的 0–0.50 秒 |
| 悬停概要、点击沿自身几何弹出 8 px + 引线 + 详情卡 | ix.mjs | 搬。格子四边各外扩 4 px 抬起;引线从格子右沿水平接到右栏;详情卡就是右栏:盖在概览上、从引线接上的那一点沿左沿展开,内容沿用原来的详情(作答卡、为什么在这里、依据、来源处的回应、本轮变化、版本、时间) |
| 自动讲一遍 | 手表:顶层直接打开、不带 ?frame=、没有 referrer 时演一次 | 搬,约 10 秒,见 3.4 |
| 4 秒节拍器、首尾循环、粒子云与余辉 | 脉冲、拨盘 | 不搬。树图只在换轮、作答、展开三种时刻动 |
| 手势(长按、左滑、下拉跟手) | D | 不搬。桌面上悬停就是概要 |
2 数据(data)
2.1 规则
| 量 | 怎么算 | 出处 |
|---|---|---|
| 英雄数字:N 件等你回应 | 这一层(默认根层)里 currentAsk(s, id) 不为空、且本页没有对这同一个问题作答的件数 | variants/shared.mjs currentAsk;app.mjs 的 waiting();与 D 的 compose().waiting 同口径(D 也只数根层) |
| 格子与面积 | 根层 foldLayer 折掉 1 份且不问你的小事成“另有 N 件小事”,layoutFill 按 rank 铺满 64 份,留白是最后一格 | records[i].state.root.items[].share;spatial-kit.mjs foldLayer;a-layout.mjs layoutFill |
| 本轮动了的格子 | 相邻两帧同一块板(1440×900 下 738×521 px)上:进场(新 id,含从小事块里出来的)、离场(含收进小事块的)、移位或缩放(矩形差 > 0.5 px)、只换字(矩形不变、diff.changes 里有它) | 同上;records[i].diff.changes |
| 本页作答 | createAnswerBook:按下即 saved,回放到下一帧变 acknowledged;按条目找最近一条,再与这一帧的 currentAsk 比:同一个问题 = 回执,另一个问题 = “你答过上一版”,没有问题 = 关上后的小字 | spatial-kit.mjs createAnswerBook;respond.mjs responseStatus |
| 真实作答 | event.feedback[] 带 ask 指向这个问题 | 同 拨盘 data.json 的 rules.openAsks |
| 灰点 | 本轮 frame.events 里采用且 relatedIds 归到此刻板上某一格的,一条一粒、落进那一格;最多 40 粒,超出按 event.at 排序后等距抽样(不是取前 40);其余不画 | motion/dial-model.mjs rounds[i].events[].root(与拨盘、手表同一条归属规则);时刻取 frame.events[k].at |
| 新登记的问题 | 这一帧的 currentAsk 的 id 不在上一帧任何格子的问题里 | frame.asks |
2.2 composite:f21 → f22 → 批准 #13572 → f23
composite 每 12 小时一轮。样张取 D 的同一处(home-motion.md 2.4):f22 有一件工作评审正好能在格子里批准,真实的下一步也是批准,f23 是两周最大的一次让位。
| 时刻 | 事实 | 出处 |
|---|---|---|
| f21 08-13 12:00「#13567:复用防护可被绕过」 | 97 条输入、采用 31;6 件等你回应(母亲九月住哪、猫的药量、派对人数、Ture 的 VAB、开学同意书、Göran 抬棚屋);格子 9 块,小事块里 12 件 | frames[21];waiting() |
| f22 08-14 00:00「热浪第一天:药量超过失明线」 | 172 条输入(两周第二多)、采用 34,其中 26 条归到板上的格子(#13572 那格 8 条);5 件等你回应;母亲 24 份、猫 12 → 24、pr-reviews-0814 新增 6 份(“#13572等你评审,另3个PR待审”,ask-20 approve,格子里有“批准 / 要求修改”)、Ture 3、开学 1,小事块 12 件 → 6 件,留白 2 → 0;离场 5 格:派对、棚屋撤下,#13567 并入 #13572 那格,旧的开学同意书并入新的一格,留白用完 | frames[22];records[22].diff;currentAsk |
| 本页批准(模拟) | 5 → 4;那一格原地收成回执“✓ 你批准了 · 已保存,等下一轮整理”,面积不变,别的格子一格不动 | 本页作答 |
| 11:38(真实) | 你在 GitHub 上批准了 #13572:feedback: {ask: ask-20, action: approve, via: source} | f23 的事件 |
| f23 08-14 12:00「打了诊所;母亲自己定了」 | 115 条输入、采用 34;3 件等你回应;猫 24 → 12(打了诊所)、母亲 24 → 6(自己定了,问题随改写失效)、master CI 从小事块里出来 1 → 6、#13572 那格 6 份不变但问题换成 ask-22“三个旧 PR 推到哪天再看?”;root.used 64 → 39,留白 0 → 25,两周最大,与拨盘 largestSingleYield 同一处 | frames[23];records[23] |
| 回执 | 这一格的问题换了,现在的页面取不到回执;照 2.1 的规则,它写新问题,下面一行“✓ 你批准了上一版 · 12:00 已收到” | 本页作答 |
这一轮动了几格:f21 → f22 进 2、出 5(其中 2 格是合并飞进目标)、移位 4;f22 → f23 进 2(master CI 从小事块出来、留白长出来)、移位缩放 6。
2.3 story:f03 → f04 → 选「固定模板」→ f05
story 数据集不定时、每轮 3 条输入,没有 approve 的问题,所以作答在详情卡里。样张同 D 的 story 对照(home-motion.md 2.4):
| 时刻 | 事实 | 出处 |
|---|---|---|
| f03 10-12 20:30「厨房渗水,物业尚未确定上门时间」 | 4 件等你回应(导出模板、周六见妈妈、API 账单、面谈职责);10 格 | frames[5] |
| f04 10-13 09:20「两条提醒来自同一张2910元API账单」 | 3 条输入、采用 2;4 件等你回应(数字不动);两条账单线索 invoice-mail、invoice-chat 并入新的 expenses(6 份),周六 6 → 12 | frames[6];records[6].diff |
| 本页在详情里选「固定模板」(模拟) | 4 → 3;导出那格(12 份,ask-03 choose)收成回执,面积不变 | 本页作答 |
| 14:34(真实) | 林苒在团队群里回复“这版先不做模板编辑”:{ask: ask-04, action: fixed, via: reply},指向子事项上的同一问 | f05 的事件 |
| f05 10-13 14:40「远川两店库存口径不一,总库存暂不承诺」 | 3 件等你回应(数字不动:让出的份额被新问题接走);导出 12 → 6(“导出发布已定固定模板”,问题关上,回执留成小字)、试用 12 → 24(新问题“今天对远川怎样表态?”)、费用 6 → 3,留白 3 → 0 | frames[7] |
2.4 两周的换轮工作量与英雄数字
| composite | story | |
|---|---|---|
| 换轮次数 | 28 | 36 |
| 板上的格子(含小事块与留白) | 6–16 块 | 2–10 块 |
| 一次换轮动了的格子 | 中位 11、最多 18(f15 → f16);f26 → f27 只换字 2 格,一格不移 | 中位 8、最多 11 |
| 其中移位或缩放 | 几乎每轮都过半:条带按 rank 铺,一件变了,同一条带和后面条带都要让 | 同左 |
| 从小事块里出来 / 收进小事块 | 一次最多 2 / 5 | 没有小事块 |
| 英雄数字 | 0–8:f00 为 0,f04、f08、f15 为 8,f23–f25 为 3 | 0–4 |
试过给条带搜索加一项“离上一帧的位移”代价(λ 从 0.05 到 1),28 轮里移位的格子只从 201 减到 178,代价是更扁的格子;所以不改布局,只把这些移动编排清楚。
3 故事(story)
一个点。 面积就是注意力,而它只在整理落地时挪:你在格子里批准一件,它先在原处收成回执,面积一份不让;等 Agent 下一轮整理读到了,份额才连同别的格子一起,从旧位置走到新位置。
英雄数字:N 件等你回应。 composite 的样张是 6 → 5(换轮)→ 4(你批准)→ 3(下一轮整理)。它是这块板唯一的大字(板头左侧,22 px 等宽,位置与字号永不变),回答场景的第一问“这两周里有几件等我”;只在两类时刻动:一轮整理落地,你作答或撤回。D 说的是同一个数,手机上排成卡,这里铺成面积。“本轮动了几格”不做成英雄数字:它几乎每轮都过半,说的是布局,不是你。
3.1 三拍
| 拍 | 时长 | 发生什么 | 观众此刻 |
|---|---|---|---|
| 1 换轮 | 约 1.0 秒(另有 1.6 秒的新问题描边) | 先换字;本轮采用的输入化成灰点从左栏流进格子;0.40 秒数字翻一格;离场 → 移位 → 进场各走一段:撤下的原地缩小淡出、并入的飞进目标、收进小事块的飞进小事块;留下的沿自己的矩形长缩到新位置;新的从小事块里或原地揭开 | 这一轮挪了什么,现在几件等我 |
| 2 作答 | 0.46 秒 | 在格子里按“批准”:提问与按钮 0.12 秒收起,回执一行在原处出现、空心勾描出,数字减一;格子不动、面积不变;详情里作答同样,回执也落在格子里 | 我答了,但还没让 |
| 3 下一轮 | 约 1.0 秒 | 先确认回执:0–0.30 秒空心勾填实,写“已收到”,其余不动;0.40 秒数字翻;然后份额才让出去:答掉的、失效的缩小沉到后面,接走份额的长大,留白长出来;回执跟着那一格走 | 答掉的去了哪,面积让给了谁 |
3.2 节拍表(秒,从切点起)
| 节拍 | 切点 0 | 回执 / 概念层 | 英雄数字 | 离场 | 移位 | 进场 | 其他 |
|---|---|---|---|---|---|---|---|
| 换轮 | 日期、旁白、整理依据、修订号、左栏、右栏概览、时间轴硬切 | 回执填实 0–0.30;灰点 0–0.50 | 0.40 起 0.26 | 0.40–0.64 | 0.48–0.84:旧字 0.48 起 0.16 淡出,新字 0.60 起 0.24 淡入;“N 份”与“已用 / 留白”0.84 翻 | 0.60 起 0.36,错开 40 ms,最多 8 个 | 新问题描边 0.96 起 1.6 |
| 作答 | 回执的文字 | 空心勾 0.12–0.30 | 0.20 起 0.26 | — | — | — | 提问与按钮 0–0.12 收起,回执与“✓ 你批准了”0.10–0.30 出现 |
| 撤回 | — | — | 0.10 | — | — | — | 回执 0–0.10 收起,提问与按钮 0.10–0.30 回来 |
| 拖时间轴、对比、改尺寸 | 同换轮 | 无 | 0 | 0–0.20 | 0–0.32 | 0.08 起 0.30 | 同时起步 |
| 下钻、放大小事块 | 面包屑 | 无 | 0 | 0–0.37 | 经锚点格子缩放 0–0.46 | 0.05 起 0.41 | 慢动作把所有时长乘 3 |
连按 ←/→ 时,上一段直接跳到终态,再从那里开始下一段。prefers-reduced-motion 时一切直接到终态。
3.3 两层与约束
- 信息层(约八成)。 格子、面积、字、英雄数字、回执、引线与卡。只在节拍上动。颜色沿用树图自己的:格子按根层事项的颜色,墨点 = 登记了回应路径,赭色描边 = 本轮有变化,绿色 = 你的回执。
- 概念层(约两成)。 只有灰点:本轮采用且归到板上某一格的输入一条一粒,从左栏右沿流向中间、落进那一格、落地即灭,最多 40 粒(超出按输入时刻等距抽样);没采用或归不到板上的不画。只在换轮的 0–0.50 秒出现,静止时为零;不出文字与数字、不用颜色;本轮 0 条输入(story 的两次 review)就不落。
- 绝不动。 英雄数字的位置与字号;板的边界;排序规则(rank);格子的字号。动画只表现
layoutFill排出的结果,不发明位置。 - 每格一次只走一段。 一次节拍里每格从旧矩形到新矩形只有一段运动,不先缩后长;字不拉伸。
- 先后。 切点换字 → 你做过的先确认(回执)→ 英雄数字 → 离场 → 移位 → 进场 → 新问题描边。
- 不提前。 下一轮整理落地之前,不动面积,回执不填实;悬停在节拍中途时,概要卡不说出后面节拍的事。
- 预算。 只动
transform与opacity;正文用反向缩放留在色块左上角,不用clip-path动画。
3.4 自动讲一遍
只在顶层直接打开、地址不带 ?frame=、没有 referrer 时演一次;任意键或点按跳过,跳过时直接停在终态;视图栏的“重演”在顶层打开时都有。画廊、合页、体验站弹框(都在 iframe 里或带 referrer)与带帧的地址一律静止;prefers-reduced-motion 时也不演。
composite 约 10 秒:停在 f21,0.9 秒后换到 f22(第 1 拍);2.6 秒悬停 #13572 那一格看概要;4.4 秒替你按下“批准”(第 2 拍);6.0 秒换到 f23(第 3 拍);7.8 秒再悬停那一格,概要写“你批准了上一版 · 已收到”和新问题;10 秒收起。story 同样三拍:f03 → f04(两条账单线索并入一格)→ 展开导出那格、在详情卡里选“固定模板”→ 收起 → f05。讲一遍替你按下的回答与真实数据里你的下一步相同,按下后和你自己按的一样,可以撤销;“重演”先撤掉它再演。
3.5 交互(共用 motion/ix.mjs)
- 悬停。 鼠标移到格子上(含格子外沿 10 px 内的板边),或 Tab 到它(每格一个隐藏按钮,在“对比”之后):出概要卡,写来自哪条流、完整标题、几份与本轮怎么变、本轮进了几条输入、开着的问题或你的回执;其余格子降到七成;连播与在途的节拍停在当前时刻。离开 0.15 秒宽限,卡 0.12 秒淡出,再过 0.4 秒接着走。悬停输入栏或概览里的问题,对应格子描边(原有)。
- 展开。 点格子、点它的概要卡、Tab 后按 Enter,或点输入栏与概览里指向它的条目。0 秒切点:旁白标题切到“这一件 · …”;0–0.3 秒格子四边各外扩 4 px 抬起;0.4–0.6 秒引线从格子右沿水平画到右栏;0.6–0.9 秒详情卡从引线接上的那一点沿右栏左沿展开、再向右铺满,盖在概览上。卡里是原来的详情,作答照常在卡里做,回执同时落在格子里。
- 收回。 Esc、点空白、再点同一格、卡上的“↶ 概览”:卡 0.2 秒收起、引线 0.15 秒缩回、格子 0.2 秒落回。换轮、连播、合页顶栏选帧、下钻与对比立即收起。键盘打开的,焦点进卡;收回后回到那一格的隐藏按钮。
- 作答。 格子里“批准 / 要求修改”,详情卡里 A 批准、R 要求修改、其余控件;U 撤销最近一次。不再弹 toast,“已保存”只写在回执里(读屏用一个隐藏的
aria-live)。树图本身能作答,不加“去回答 →”。 - 钩子。
window.foldTree有与foldDial同名同义的hover / unhover / open / close / ixAt / paused / pointOf / ixState,另有index、go(i)、play()、stop()、beatAt(秒)(把在途节拍定在某一刻,null放开)、answer(id, action)、undo(id)、waiting()、tiles()、pick()、nearOf(id, px)、tell()、skip()。scripts/check-ix.mjs的 16 项在本页同样跑。
4 动(motion)
4.1 文件与分工
- motion.mjs:只管“一组怎么动”(原来那份克隆幽灵的实现已经没有页面在用,整份重写):节拍表
BEATS与节拍时钟createBeat(一次换轮或作答的全部动画挂在同一只表上,可停住、可定格、可跳到终态);按 id 保留格子的板createBoard(离场 → 移位 → 进场,换了字的格子逐个部件交接);英雄数字滚一格roll、一行字在节拍上翻flipText;灰点planGrains/drawGrains。 - app.mjs:编排(谁在哪一组、什么时候动);英雄数字;回执跨版本(
receiptOf);作答与撤回;可停住的连播;接 ix.mjs(命中、外沿 10 px、概要卡、详情卡的摆位与内容、弹出、降暗、隐藏按钮);讲一遍与“重演”;window.foldTree。 - styles.css:格子的尺寸类与裁字标记从格子挪到正文上(
.pin-tile.is-small …→.pin-tile .is-small …,同样的优先级),旧正文因此保留旧排版;末尾追加英雄数字、回执的勾、新问题描边、弹出的底板、键盘描边、灰点画布、详情卡与--ix-*变量。ix.css由 app.mjs 用<link>载入。 index.html、explore.mjs、shared.mjs、respond.mjs、spatial-kit.mjs、ix.mjs都没改;“重演”按钮与隐藏按钮组由 app.mjs 插进视图栏。
4.2 实现的节拍表
秒,从切点起,[起点, 时长];与 3.2 一一对应,motion.mjs 的 BEATS 里同名。
| 节拍 | 回执 / 概念层 | 英雄数字 | 离场 | 移位 | 换字(旧 / 新) | 进场(错开 40 ms,最多 8 个) | 其他 |
|---|---|---|---|---|---|---|---|
换轮 turn | 回执填实 0–0.30;灰点 0–0.50 | 0.40 起 0.26 | 0.40–0.64 | 0.48–0.84 | 0.48–0.64 / 0.60–0.84;“N 份”、留白数与“已用 / 留白”0.84 翻 | 0.60 起 0.36 | 新问题描边 0.96 起 1.6 |
作答 answer | 空心勾 0.12–0.30 | 0.20 起 0.26 | — | — | 提问、按钮、标签 0–0.12 / 回执与“✓ 批准了”0.10–0.30 | — | 格子不动、面积不变 |
撤回 undo | — | 0.10 起 0.26 | — | — | 回执 0–0.10 / 提问与按钮 0.10–0.30 | — | |
拖时间轴、对比、改尺寸 quick | 回执 0–0.20 | 0 | 0–0.20 | 0–0.32 | 0–0.12 / 0.10–0.30 | 0.08 起 0.30 | 同时起步 |
下钻、放大小事块 zoom | — | 0 | 0–0.37 | 经锚点格子 0–0.46 | 0–0.16 / 0.18–0.46 | 0.05 起 0.41 |
| 交互(ix) | 时刻 |
|---|---|
| 悬停 | 其余格子 0.15 秒降到七成(只降色块,字跟着);概要卡 0.15 秒淡入,贴在格子朝板中线的一侧;离开 0.15 秒宽限,0.12 秒淡出,0.4 秒后在途节拍与连播接着走 |
| 展开 | 0 秒旁白标题切到“这一件 · 短名”;0–0.3 秒格子背后一块同色底板四边各外扩 4 px(带一圈墨线与投影,过冲约 3.7 %);0.4–0.6 秒引线从格子右沿水平画到右栏左沿;0.6–0.9 秒详情卡先沿右栏左沿、再向右铺满 |
| 收回 | 卡 0.2 秒收起、引线 0.15 秒缩回、底板 0.2 秒落回,然后标题切回 |
4.3 取舍
- 字不拉伸、色块裁字。 正文放在色块里面,色块沿自己的矩形平移与缩放,正文按同一采样(每段 14 个关键帧、线性插值)反向缩放,钉在色块左上角、被色块裁住。不用
clip-path,也不在关键帧里写transform-origin,只动transform与opacity。 - 换了字的格子逐个部件交接。 内容变了的格子把旧正文留成“幽灵”(旧尺寸、旧尺寸类,所以旧字不重排),新旧正文按
data-part(标签、份数、标题、提问、回执、摘要、页脚)配对:同字的从旧位置滑到新位置,换字的旧淡出、新淡入,份数与留白数在落定时一帧翻,不交叉淡化。 - 回执只有一个部件名。 回执在格子里(按钮的位置)或标题下一行,答的是这一版、上一版还是已关上的问题都叫
receipt;下一轮读到时新旧回执配成一对,新的一行立刻写“已收到”、实心点 0–0.30 秒放大填进空心勾,再随格子移位,不在半路淡出重来。 - 回执跟着条目走。
receiptOf按条目取本页最近一次作答,再与这一帧的问题比:同一个问题 = 回执,问题换了 = “你批准了上一版 · 已收到”,问题关上 = 一行小字(story f05 导出那格“你选了「固定模板」· 已收到”)。读屏用一个隐藏的aria-live,不再弹 toast(“复制 JSON”的提示保留)。 - 作答不被停住。 悬停与打开的卡会停住在途的换轮与连播;作答与撤回是你自己的动作,回执在你悬停或看卡时照常画完。
- 详情卡就是右栏。 ix 的详情放在右栏的位置上、和右栏同高,内容是原来右栏的这一件(作答、为什么在这里、依据、版本……),概览留在它底下;作答在卡里做,回执同时落在格子里,卡里的字原地换掉,位置与滚动不变。右栏在板子下面时(窄屏)卡也在那里,引线向下,打开后把它滚进视野。
- 弹出不拉字。 外扩 4 px 是格子背后的一块底板按比例放大,格子本身与字不缩放;打开时板子的裁切临时放开,贴边的格子也能外扩。
- 灰点。 只画采用且归到板上某一格的输入(拨盘的归属规则
rounds[i].events[].root,收在小事块里的落进小事块),一条一粒,从左栏右沿出发、落进那一格、落地即灭;最多 40 粒,超出按event.at排序后等距抽样(不是取前 40);没采用或归不到板上的不画,不再半路熄灭。半径 1.2 px、最浓 0.24(初版 1.7 px、0.34 的约七成),时长不变。只在顶层、不对比、左栏在板子左边时画;story 的 review 轮没有输入,就不落。悬停时降到四成。 - 连播。 一轮 = 换轮 1.0 秒 + 读 max(2.2, 2.9 / 速度) 秒;悬停、打开的卡停住累计,移开 0.4 秒后从停住处接着计时(第一轮也要等满,所以按“播放”后约 3.9 秒才换第一轮,与原来的 3.2 秒相近)。
- 链入。
?item=落地时那一件直接以卡打开(等在途节拍走完);打开卡时地址带上item,收回时去掉。 - 减少动态时 不建节拍,一切直接到终态,ix 的弹出、引线与卡也直接到位,不讲一遍。
4.4 与故事的出入
- composite 那一格自己没有让。 f22 批准 #13572 之后,f23 这一格仍是 6 份(问题换成“三个旧 PR 推到哪天再看”);这一轮让出份额的是母亲 24 → 6、猫 24 → 12,留白 0 → 25 长出来,回执跟着这一格从左下移到上排中间。“答掉的缩小沉下去”在 story 里照字面发生:导出那格 12 → 6、问题关上、回执留成小字。
- 悬停也停住新问题的描边。 描边是换轮这一拍的最后一段(0.96 起 1.6 秒),讲一遍 7.8 秒的悬停落在它中途,描边就停在半透明处,直到移开。照“悬停停住在途节拍”的规则没有特判。
- 两个计数。 英雄数字只数这一层的格子(与 D 同口径),右栏概览的“N 个问题等你”数的是全部开着的问题(含子事项);story f04 两者是 4 与 6。没有改右栏。
- 概要卡里回执写“已收到”的门槛 用本轮输入落定(0.50 秒),不是回执填实的 0.30 秒:ix 的卡只按 inputs / share / asks 三道门槛重写。
- 引线穿过右边的格子。 引线从被打开那格的右沿水平走到右栏,路上的格子已降到七成,没有绕行。
- 760–999 px 的版式(右栏在中间一列、最右一列空着)是原有的,卡跟着右栏走,没有改版式。
- 灰点改少改淡(上线后)。 初版本轮输入两条一粒、最多 120 粒,没采用的也飞出去、在半路熄灭。用户看后嫌换轮时飞的小点多,改成只画落进格子的:采用且归到板上某一格的一条一粒,上限 40,超出按
event.at等距抽样;半径与最浓各降到约七成(1.7 → 1.2 px,0.34 → 0.24),时长 0–0.50 秒、落地即灭、悬停降四成、减少动态时不画都不变。composite f22 → f23 由 59 粒(落进格子 18、半路熄灭 41)降到 34 粒(这一轮采用的 34 条都在板上);f07 → f08 采用 94 条,只画 40 粒;story f22 → f23 三条都采用,由 2 粒变 3 粒。
5 导(direct)
定格用 foldTree.beatAt(秒) 把在途节拍(含之后新起的)定在同一时刻,用 foldTree.ixAt(秒) 定住展开;指针用 CDP 真移、真点。1440 × 900,打开 GPU。静帧只留在本机,不入库。
| 定格 | 看什么 |
|---|---|
| f21 → f22,0.62 秒 | 英雄数字已翻到 5;撤下与并入的格子已淡去,母亲、猫与 Ture 在长缩,旧标题将尽、新标题刚起;“已用 62 / 64 · 留白 2”要到 0.84 秒才翻 |
| f22 在格子里批准 #13572,0.30 秒 / 终态 | 按钮收起,回执“你批准了 · 已保存 · 等下一轮整理”在原处出现、空心勾描出,“✓ 批准了”换掉标签;数字 5 → 4;别的格子一格不动,右栏概览里同一条也成了回执 |
| f22 → f23,0.30 秒 / 0.70 秒 / 终态 | 0.30 秒回执已写“你批准了上一版 · 已收到”、勾已填实,格子都没动,灰点在落,数字还是 4;0.70 秒猫与母亲在缩,待审 PR 带着回执移到上排;终态数字 3、留白 25 |
| 悬停待审 PR(f23) | 其余降到七成;概要卡写“✓ 你批准了上一版 · 已收到 · 12:00”与新问题 |
| 点开待审 PR,0.9 秒 | 底板外扩、引线到右栏,详情卡铺满右栏,卡里是“你批准了(上一版)”的回执与新问题;标题“这一件 · 待审 PR” |
| story:f03 → f04 → 选「固定模板」→ f05 | 两条账单线索并入新的费用一格;导出那格在详情卡里选「固定模板」,卡里与格子里的回执同时出现,数字 4 → 3;f05 导出那格 12 → 6、问题关上,回执留成“你选了「固定模板」· 已收到” |
静帧:composite-* 与 story-* 各 8 张(换轮 0.62 秒、作答 0.30 秒与终态、下一轮 0.30 秒 / 0.70 秒 / 终态、悬停、展开 0.9 秒),只在本机。
6 交付(ship)
6.1 性能
无头 Chrome(打开 GPU),1440 × 900,不降速;每一拍从切点起量 2.7 秒 requestAnimationFrame 间隔,每拍量两次(第二次是同一页上再演一遍)。sync 是切点上那一次同步工作(重画左栏、右栏、板,量一次部件位置,建动画)。
| 节拍 | 格子(移位 / 进场 / 离场 / 换字) | sync ms | fps | p95 ms | 最长一帧 ms |
|---|---|---|---|---|---|
| composite f15 → f16(两周动得最多) | 11 / 3 / 1 / 9 | 26 / 29 | 59.3 / 60 | 16.8 | 33(切点)/ 16.8 |
| composite f21 → f22 | 4 / 2 / 5 / 4 | 18 / 14 | 60 / 59.6 | 16.8 | 16.8 / 33(切点) |
| composite f22 → f23(批准之后) | 6 / 2 / 0 / 6 | 20 / 13 | 59.6 / 60 | 16.8 | 33(切点)/ 16.8 |
| story f03 → f04、f04 → f05、第 15 → 16 帧 | 8 / 1 / 2 / 4 等 | — | 60 | 16.8 | 至多一帧 33(切点) |
| 点开一格的详情卡 | — | — | 60 | 16.8 | 16.8 |
| 在格子里作答 | — | — | 60 | 16.7 | 16.8 |
长帧只在切点上,至多一帧;之后全程 16.7 ms。页面打开后的第一次换轮另有一帧 230–300 ms(显卡进程第一次建图层),同一页再演就没有。CPU 剖析:切点的同步工作里板占三分之二(fitTile 量字、部件的一次布局读取),其余是左栏与右栏的重画。
6.2 检查
make check通过。node scripts/check-ix.mjs(默认数据与--data story):本页 16 / 16,其余各页照旧全过(pulse、dial、watch、notation、multiples、timing、yield 各 16 项,home 23 项)。本页的条目追加在PAGES末尾:?frame=22停住、?frame=3连播,挑板子左沿最大的一格,9 px 外是板子左边的空白,Tab 从“对比”起。node scripts/shoot-forms.mjs --only original,all --gpu --check all-sync(两份数据):没有页面报错,合页逐节报告所在帧、顶栏选帧后全部跟上。npm run site:build通过;画廊预览site/previews/original.webp按工作环境说明的参数重截(英雄数字与视图栏变了)。- 讲一遍:顶层直接打开时两份数据各演一次(约 10 秒);第 3 秒按任意键,直接停在 f23(story 为 f05)、回答已在;“重演”先撤掉上次替你按下的回答再演;带
?frame=、在 iframe 里、减少动态时都不演。 - 减少动态:换轮、作答、展开都直接到终态,页面上没有在途动画。
- 点小事块里的一件:铺开小事块,那一件的卡在缩放走完后打开;
?item=链入同样以卡打开。
6.3 没做与拿不准的
- 切点那一帧要做全部同步工作(重画左栏、右栏、两块板,量一次部件位置,建动画),本机不降速 13–29 ms,常常超过一帧;之后全程 16.7 ms。没有像 D 那样在 4 倍降速下量。
- Safari:本次只在 Chrome 无头里看过;色块里反向缩放的字、
:has()与min()写的不透明度在 Safari 27 里是否一样,需要真机看一眼。 - 小事块里的一件(1 份、不问你)不在板上,点它仍是铺开小事块、在那一层打开它的卡;收在小事块里时没有自己的概要卡。