切换外观
拔河 · 参考、数据、故事、交互与实现
状态:已实现,待审阅(2026-10-28,分支 codex/case-tug)。第四轮候选的 01 拔河,按第三轮的六步流程(reference → data → story → motion → direct → ship)做成完整形式 sketches/tug.html。第 1–4 节在任何东西动起来之前写下;第 5–6 节记实现中的取舍与验证。起点是探索代理画的静态草图(1440×900,读 composite f26,只在本机 scratchpad,不入库),用户看过草图说可以往下做。两层法则与交互标准见 CONCEPTS.md,同类页面的范本是 yield-motion.md 与 timing-motion.md,交互取共用模块 motion/ix.mjs。
1 参考:看过的图与手法
参考图在本机 .local/inspiration-r4/(不提交),条目编号见 INSPIRATION.md 第四轮。逐张看过的:
| 条目 | 看的图 | 从画面上读到的手法 | 搬什么 | 不搬什么 |
|---|---|---|---|---|
| #89 棋局复盘的评估条与评估曲线 | b-chesscom-blunder.png、b-chesscom-review-panel.png、b-lichess-analysis.png | 棋盘左边一根黑白竖条,白色的长度就是白方的优势,条顶写“3.1”;复盘面板里一条评估曲线,中线是均势,曲线上方深、下方白;红色竖线是当前这一步;面板下面是双方各类着法的计数 | 左边一根 64 份高的评估条,此刻的“家里 : 工作”写在条顶;右边一条两周的评估曲线,中线持平;一根此刻线;曲线下方是你的计数(回应柱) | “最佳着法”式的打分、好坏着色的点、“?!”记号:状态里没有“最好的一轮”作参照,份额也没有对错 |
| #54 Playfair 丹麦—挪威进出口图(文字条目,第二轮的图本机已不在) | — | 两条曲线之间的面积按差额方向着色,直接写上“Balance against / in favour of” | 曲线与中线之间的面积按领先方着色:墨面向下鼓是家里领先,白面向上是工作领先,看的是差,不是两个量本身 | 两条曲线:这里只画差,两边各自多少写在条上 |
| #88 纽约时报 2016 年选举指针 | b-needle-2016.jpg | 三个半圆表,各配一句大字结论(“306 Trump”),颜色只表示两党;刻度上的虚线把历史值钉住(“OBAMA '08”) | 一句大字结论就是英雄数字“40 : 23”;两周极值钉在标题与曲线最低点的注上 | 指针的随机抖动:动起来的东西会被读成“有新事实”,这里静止时为零运动 |
| #84 F1 计时塔 | b-f1-race-tv.png | 20 行里只把第 1、2 名之间撑开,写一个全屏最大的差值“+0.038”,其余不动 | 一轮只演一处:差距向哪边移了几份写在那一轮的点下,只标挪得多的几轮 | 撑开名次的版式 |
| #75 Apple Liquid Glass 的让位的时间 | a-liquidglass-fluidtime-frames.png | 通知一条条叠上来,“9:41”的字高被压矮,屏幕总高不变,数字的形体就是剩下的空间 | 条的总高永远是 64 份:一边涨,另一边或留白就被压短,压的是同一根条 | 玻璃折射与高光(拟物) |
草图(scratchpad r4/tug.html,截图 r4/shots/tug.png)已经定下第一眼:左边评估条上白(工作)下黑(家里),中间斜线是留白,条旁写每件事的份数;右边曲线中线持平,墨面向下鼓起是家里领先,8/12–8/14 深深坠下,8/14 12:00 一下弹回;曲线下一排朱红短柱,向上是你在工作上的回应、向下是家里的;四处大挪动写“家里 +21 / 母亲换水管 +12 / master CI −6”。草图是静态的单帧,名字与份数从 scratchpad 里导出的 JSON 算,没有接回放、没有交互。
草图哪里讲不动。
- 看不出“拉”。 条只画此刻,曲线一次画完;同一件事(主水管)从 1 份长到 24 份、把绳子往家里拉,这一下没有被演出来。
- 条和曲线是两套刻度。 条一份 7.4 px,曲线一份差 3.6 px,持平线和条没有关系。
- 对不上要自己找。 “8/7 份额家里 39 : 工作 25,你却在工作上动了 13 次”是全图最值得看的一处,草图写成一条注;其余十轮同样对不上,要自己比柱子和曲线。
- 草图的“家里 +21”会被读成家里多了 21 份。 那一轮家里 30 → 40、工作 34 → 23,挪的是差距 21。
- “流”是逐帧推的。 草图按这一帧的来源加这一轮的输入投票,同一件事可能这一帧算家里、下一帧算工作;composite 两周里没有翻过边,story 有 6 件在只有“用户消息”来源的帧里归不了边(第 2 节)。
两层与占比。 这是复盘工具,会反复打开、逐轮比对,所以照让位图的规矩:概念层一成以内,静止时为零运动;“丝滑”靠同一件事的位置连续(条上的每一段从上一轮的位置滑到这一轮),不靠粒子。概念层只有一种东西:一轮里换了边的格子,被分界扫过时先灰一下(约 0.28 秒)再落成新的颜色,一格一份,个数就是这一轮换手的份数;它只在条里,不出条,画在叠在图上的一张 canvas 上,平账后清空。信息层(条、英雄数字、曲线、留白刻、回应柱、注、此刻线、侧栏)只在节拍上落定:数字在一轮落定的那一刻硬切。
2 数据:两周里的拉锯
默认数据集是 composite(Jonas 的两周,29 帧,f00 是空状态,帧号 fNN 就是序号 NN);?data=story 是林苒的两周(37 帧,cold-00 至 f34,frames[17] 是 f15)。页面用 node 能导入的同一套推导现算:variants/shared.mjs 的 records(replayScenario)与 currentAsk,sketches/stage.mjs 的 askLife、responseSentence。核对用的脚本只放在本机 scratchpad,不提交;不改 scenario、shared、engine 与演示数据,不加字段。
口径
- 份额。
records[i].state.root.items[].share,只看根层;留白 = 64 −records[i].state.root.used。家里 H、工作 W 是两边根层份额之和,H + W + 留白 = 64。 - 事项归边(家里还是工作)。 一件根层事项两周里每一版(
records[*].snapshot.items,含子项)引用过的来源sources[].ref去重后,每条按它所在的流投一票:composite 用事件的stream;story 没有stream,按事件(或来源)的channel开头归入工作、家庭、生活、订阅,规则同 motion/dial-model.mjs 的车道(团队|客户|监控|部署|反馈|合作|招聘|供应商|财务是工作,家人是家庭,物业|同楼|团购|快递|随手记|日历|定时是生活,资讯|促销|官方是订阅),你自己发给 Agent 的“用户消息”不投票。得票最多的流是工作就算工作,其余(家庭、生活、社区、订阅)都算家里,所以“生活问答”(问答回复、9/14 航班、漫画)并入家里。一件事两周只归一边,不随帧翻边,和身份色一样是两周固定的。- composite 60 件根层事项:工作 38 件、家里 22 件,没有平票、没有归不了边的;结果与两周小倍数“按流”着色用的
live.mjsstreamOf相同。 - story 10 件:工作 8 件(发布、试用、筛选、账单、查重、苏禾、链路、费用),家里 2 件(妈妈、厨房),没有平票。
- 草图按“这一帧的来源 + 本轮输入半票”逐帧投票。composite 下两种投法每一帧的 H : W 都相同;story 下逐帧投票在 f01–f06 把链路算成家里、在 f30–f34 让妈妈与苏禾归不了边(那几帧它们只引用了“用户消息”),两周固定的投法没有这个问题。
- composite 60 件根层事项:工作 38 件、家里 22 件,没有平票、没有归不了边的;结果与两周小倍数“按流”着色用的
- 回应。 一条反馈链接(
frames[i].events[].feedback[])算一次,归到它target所属根层事项的那一边,记在事件所在的那一轮。composite 160 条全是via: source(在 GitHub、问答站、iMessage、电话里直接动手),按target归的边与按事件stream归的边逐条相同;story 15 条(12 条回复 Agent、3 条按钮),其中 10 条来自“用户消息”,只能按target归边。- 次数不等于用力:GitHub 上的评论一条一次,给母亲打 24 分钟电话也是一次。
- 没有挂到事项上的本人动作不算:composite 里你本人发出的事件,工作流 405 条、其余 178 条,挂了反馈链接的是 91 与 69 条。
- 问题。 开着的问题用
askLife[i].open(currentAsk,含子层),关上分答掉与失效(askLife[i].answered、lapsed),与让位图、回应时序同一口径。 - 差距与挪动。 差距 d = H − W(正是家里领先);一轮的挪动 = d(i) − d(i − 1),“向家里 21”是差距向家里移了 21 份,不是家里多了 21 份。曲线上只给 |挪动| ≥ 14 的轮写注,注里再列这一轮份额变动最大的两件。
- 换手的格子。 条从上往下是 64 格:工作占前 W 格,留白占中间,家里占后 H 格。一格在上一轮和这一轮所属的一边(工作、留白、家里)不同,就是换了手;个数是概念层的格数。
composite:两周的拉锯
逐轮 H : W(留白):f01 33 : 24(7)、f02 30 : 34(0)、f03 40 : 23(1)、f04 40 : 24、f05 31 : 27(6)、f06 35 : 29、f07 33 : 31、f08 39 : 25、f09 42 : 22、f10 33 : 17(14)、f11 33 : 18(13)、f12 36 : 27(1)、f13 37 : 27、f14 40 : 22(2)、f15 38 : 24(2)、f16 40 : 21(3)、f17 41 : 23、f18 55 : 9、f19 58 : 6、f20 59 : 5、f21 51 : 11(2)、f22 55 : 9、f23 25 : 14(25)、f24 31 : 20(13)、f25 34 : 18(12)、f26 40 : 23(1)、f27 40 : 23(1)、f28 44 : 20。没写留白的是 0。
| 数 | 值 | 来源与规则 |
|---|---|---|
| 此刻(英雄数字) | f26(8/16 00:00)家里 40 : 工作 23,留白 1 | records[26].state.root;家里:主水管 24、猫看病 6、后刹 3、母亲换水管 3、Ture 发烧 1、开学文件 1、漫画 1、风扇 1;工作:FTPS 计时 6、待审 PR 6、master CI 6、65KB 上传 3、Kerberos 1、rc1 1 |
| 两周极值(标题) | f20(8/13 00:00)家里 59 : 工作 5,留白 0 | 28 轮里 |d| 最大的一轮;家里:母亲换水管 24、猫看病 12、后刹 6、小龙虾派对 6、Ture 发烧 3 和 8 件 1 份;工作只剩 5 件各 1 份 |
| 谁领先 | 28 轮里家里领先 27 轮,只有 f02(8/4 00:00)工作 34 : 30 | d 的正负;f03 一轮就翻回家里 |
| 同样的 40 : 23 | f03(8/4 12:00)、f26、f27 | 比分一样,里面的事全换了:f03 是母亲换水管 24、猫看病 12,f26 是主水管 24 |
| 份额·轮(两周) | 家里 1113 : 工作 576(家里约 66%) | 28 轮的 H、W 各自求和 |
| 你的回应(两周) | 160 次:工作 91、家里 69 | 反馈链接,按 target 归边;单轮最多工作 13 次(f08)、家里 10 次(f20) |
| 头九天(f01–f17,8/3 12:00–8/11 12:00) | 份额·轮家里 621 : 工作 418(约 60% 家里);你动手 101 次,工作 72、家里 29 | 同上,按轮段求和 |
| 之后(f18–f28) | 份额·轮家里 492 : 工作 158(约 76% 家里);你动手 59 次,家里 40、工作 19 | 同上 |
| 对不上的轮 | 27 轮有回应,11 轮回应多的一边和份额领先的一边相反,全在 f02–f16 | 回应两边相等的轮不算 |
| 对不上得最厉害 | f08(8/7 00:00「最忙的一天,航班改经拉纳卡」):份额家里 39 : 工作 25,你在工作上动手 13 次、家里 1 次 | “落后一边的回应 − 领先一边的回应”最大的一轮(13 − 1 = 12);工作的 13 次是回环提速 5、NDEBUG 6、测试名长度 1、PSL cookie 1,家里 1 次是抬棚屋;那一轮家里的大头是母亲换水管 24 |
| 大挪动(|挪动| ≥ 14) | f03 向家里 21(母亲换水管 +12、master CI −6);f18 向家里 28(母亲换水管 +12、猫看病 +9);f21 向工作 14(复用绕过 +6、后刹 −5);f23 向工作 35(母亲换水管 −18、猫看病 −12) | d(i) − d(i − 1);动得最多的两件按 |份额变动| |
| f23 弹回 | 8/14 12:00「打了诊所;母亲自己定了」:55 : 9 → 25 : 14,留白 0 → 25;换手 30 格 | 猫看病 24 → 12(你 08:02 给诊所打了电话,答掉「Smilla 多吃了药:打诊所?」),母亲换水管 24 → 6(「母亲九月住哪?」随改写失效);与让位图 f23 的“让出 30”同一笔 |
| 换手最多的一轮(冷启动之外) | f23,30 格;其次 f10 与 f18,各 14 格(f10 是 14 格退成留白,f18 是工作 23 → 9 的 14 格变成家里);冷启动 f01 是 57 格 | 按上面“换手的格子” |
story:工作占着大头,家里翻过中线一个周末
逐轮 H : W 见页面;要点:
| 数 | 值 | 来源与规则 |
|---|---|---|
| 份额·轮(两周) | 家里 531 : 工作 1379(工作约 72%) | 同上 |
| 谁领先 | 36 轮里工作领先 32 轮;家里只在 f15–f18(10/17 10:00–10/18 17:20)领先 | d 的正负 |
| 两周极值 | f23(10/20 14:00)工作 56 : 家里 6,留白 2;f25 也是 6 : 56,取先到的 | |d| 最大 |
| 翻过中线 | f14(10/16 21:20)18 : 43 → f15(10/17 10:00)30 : 16:向家里 39(试用 −18、妈妈 +12),留白 3 → 18 | f14 有林苒的批注“改到周日也可以。这次保留最大的位置给妈妈;发布做成了就让它小一点,但别删掉”(frames[16].annotations);f15 答掉「周六 11 点见妈妈,可以吗?」,回复“改到周日 11 点” |
| 退回 | f18 36 : 16 → f19(10/19 09:10)18 : 37:向工作 39(妈妈 −18、发布 +9) | f18 你回复“已见到妈妈”;f19 是周一 |
| 你的回应 | 15 次:工作 9、家里 6;家里的 6 次全是回答登记的问题 | 单轮最多 2 次,回应柱很矮 |
| 对不上的轮 | 12 轮有回应,4 轮相反(f11、f21、f32、f33),每轮都只有 1 次 | 落后一边最多只多 1 次,所以 story 不写“对不上”的注 |
| 大挪动 | f01 向工作 31、f15 向家里 39、f19 向工作 39、f31 向家里 15 | 同上 |
3 故事
论点。 Agent 把多数份额给了家里,你的手却先在工作上;直到家里压到 59 份,你的手才跟过去。(composite:头九天份额六成在家里,你在来源处动手 101 次,72 次在工作上;8/12 起家里压到 55–59 份,之后 59 次里 40 次在家里。8/14 中午两件事一关上,绳子一下弹回,到 8/16 零点又被主水管拉到 40 : 23。)
这张图让人看见别处看不见的关系:其他形式要么画份额(拨盘、树图、小倍数),要么画你的回应(回应时序、批次记谱),没有一个把“Agent 给的”和“你做的”按同一条分界并排;小倍数按身份着色,看不出领域之间的拉锯。
英雄数字:40 : 23。 此刻(f26)家里 : 工作,写在评估条顶上,56 px 等宽,家里在前。它就是条的读数:条上黑的 40 格、白的 23 格、斜线 1 格。标题写两周的极值“8/13 家里压到 59 份,工作只剩 5 份”。讲述与连播里它随每一轮落定硬切;一轮只是路过(讲述里的快轮)时写成灰的,落在节拍上转墨。
三拍(composite 约 23 秒,讲一遍就停在 f26;帧号是页面常量,不写进数据;两拍之间夹着的轮按快轮过账,不跳过,曲线不能缺点)
| 拍 | 时长与帧 | 发生什么 | 观众此刻 | 在动 / 只在节拍上落定 |
|---|---|---|---|---|
| 0 | 0–0.4 秒,f00 | 空状态:条全是斜线(留白 64),曲线还没有点,英雄数字 0 : 0 | 一根空的条 | 什么也不动 |
| 1 手在工作上 | 0.4–6.85 秒:f01 半速,f02–f07 快轮,f08 全速再停 1.5 秒 | 第一轮 57 份落下,家里就领先(33 : 24);之后每一轮条上的段滑到新位置、曲线向右长一格、朱红柱从基线长出;柱子多半朝上(工作)。到 f08:条 39 : 25,曲线在中线下,柱子朝上 13、朝下 1,落定后这一轮旁出注“份额家里 39 : 工作 25 · 你在工作上动手 13 次,家里 1 次” | 份额说家里重,手却在工作上 | 动:条上的段、曲线的头、朱红柱、换手的格子。落定:英雄数字、条旁的名字与份数、注 |
| 2 坠下 | 6.85–16.15 秒:f09–f17 快轮,f18 全速,f19 半速,f20 全速再停 1.5 秒 | f18 母亲换水管 +12、猫看病 +9,工作 23 → 9 的 14 格先灰再落黑,曲线一步坠到 55 : 9;f20 到最低 59 : 5,最低点旁写“8/13 00:00 家里 59 : 工作 5”;这几轮的朱红柱翻到下面(f20 家里 10 次) | 家里压到底,手才跟过去 | 同上;f18 的注“向家里 28”在落定时出现 |
| 3 弹回 | 16.15–23.05 秒:f21–f22 快轮,f23 全速再停 0.6 秒,f24–f25 快轮,f26 全速再停 1.5 秒 | f23 两件事关上,30 格换手、留白开出 25 格,曲线一下弹回 25 : 14(注“向工作 35”);之后主水管 1 → 6 → 12 → 24,又把绳子拉回去,停在 f26:40 : 23,此刻线停在这一轮 | 份额不是记分,是此刻:同样的 40 : 23,8/4 是母亲和猫,今天是主水管 | 同上;停下后整页静止 |
讲完后页面就是普通的复盘工具,停在 f26(地址栏 frame=26)。
一轮怎样过账(全速 1.5 秒;半速 × 0.5 = 0.75 秒;快轮 × 0.3 = 0.45 秒;←/→ 逐帧用 0.5 秒的短版;连播用半速)
- 0 切点:顶栏日期、帧号与旁白硬切;此刻线滑到新的一轮(0.32 秒,ease-out,过冲约 3.7 %)。
- 0.10–0.70 拉:条上每一段从上一轮的位置与高度滑到这一轮(同一曲线),新的一件从它在条里的位置由零长出,撤下的缩成零;曲线的头同时从上一轮的点走到这一轮的点,墨面或白面跟着长;这一轮的留白刻跟着长;分界扫过的每一格先灰再落定(概念层)。
- 0.70 落定:英雄数字、条旁的名字与份数、侧栏“这一轮”一帧硬切;这一轮若有注(大挪动、对不上、极值),淡入 0.15 秒。
- 0.70–1.00 朱红柱从基线长出,向上是工作、向下是家里;够高的写次数。
- 1.00–1.50 静止,留着读。
两层。 信息层(九成以上):条、英雄数字、名字与份数、曲线与墨面、留白刻、朱红柱、注、此刻线、侧栏。只用纸白、墨黑、灰阶与一种朱红;只在节拍上落定。概念层(一成以内,静止为零):换手格子的灰,只在条里、只在拉的那一下。规矩:概念层不带文字、数字与颜色;不暗示信息层没有的事实,换手为 0 的轮(composite 的 f27)什么也不闪;不画配对(灰格只说这一格换了边,不说是谁让给谁)。
朱红只给一种含义:你本人的回应。 朱红柱、柱上的次数、卡里你的动作句前的小方块、story 里林苒的批注记号。开着的问题、此刻线、注一律墨黑;ix 的卡默认把问题标朱红,本页改成墨色。
动起来之前的约束
- 绝不动:条的位置与总高(永远 64 份)、持平线、曲线已经画出的部分、此刻之后什么也不画。
- 条与曲线共用一把尺:曲线一份差 = 条上半格(u / 2),持平线从条的正中(第 32 格)引出;条上的分界与曲线的头方向相反(家里涨时条上的黑往上长、曲线往下坠),两者都是“墨多了”。
- 条里的顺序两周固定:工作从上往下、家里从下往上,按第一次出现的先后排,新的一件长在靠中间的一端;同一件事每一轮都在同一个相对位置,换轮时只是滑。
- 只读回放:份额、归边、回应、问题都现算;三拍的帧号是页面常量。
prefers-reduced-motion:不讲,直接停在故事最后一帧;换轮直接到终态,不画换手的格子。- 合页顶栏选帧、画廊缩略图、体验站弹框与从别处链来的:立即停在那一帧,不讲。
story 的论点与三拍(同一时间表,约 17 秒,停在 f19)。论点照实:工作占着大头,家里只在你开口的那个周末翻过中线。拍 1“工作占大头”:cold-01 半速,cold-02–f13 快轮,f14 全速再停 1 秒(林苒的批注落在 f14,朱红 ✎);拍 2“翻过中线”:f15 全速再停 1.5 秒,30 : 16,注“向家里 39”;拍 3“周一退回”:f16–f18 快轮,f19 全速再停 1.5 秒,18 : 37,注“向工作 39”。story 的回应太少(单轮最多 2 次),不写“对不上”的注。
4 交互
与份额拨盘、整理脉冲、让位图同一套标准:悬停卡、弹出、引线、详情卡的时间与样式取 motion/ix.mjs;拔河只提供几何(这一点是哪件事、它沿哪个方向弹出、卡挂在哪里)、卡的内容与怎样停表、压暗。
“一件”有三种。
| 种 | 在哪 | 悬停(概要卡) | 点击(详情卡 + 侧栏“这一件”) | 沿自身几何弹出 |
|---|---|---|---|---|
| 条上的一段(一件根层事项,含留白) | 此刻这一轮的条;条旁的名字也算;命中区在条两侧各放宽 10 px | 短名、这一轮的条目标题、几份、这一轮前 → 后、归在哪一边及依据(“来源 37 条全来自家庭流”)、此刻开着的问题与期限、到此刻你在它上面动手几次 | 详情卡:份额逐轮小条图、你最近的回应(动作句)、开着的问题(“去回答 →”)、摘要。侧栏“这一件”:两周的份额与归边、你在它上面的全部回应(按轮)、问题的开闭、摘要依据来源与版本 | 向右 8 px(朝名字与卡),引线从段的右端沿水平出到卡 |
| 曲线上的一轮 | 此刻及之前每一轮的竖带(点左右各半格,曲线区内) | 时间与帧号、H : W 与留白、差距向哪边移了几份、动得最多的两件、这一轮你动手几次(工作 / 家里) | 详情卡:两边各自的变动、这一轮你的回应、这一轮登记过的问题此刻的状态。侧栏“这一轮”换成那一轮 | 点沿自己的竖线离开持平线 8 px,引线从点水平出到卡 |
| 一根朱红柱 | 此刻及之前每一轮的上柱(工作)、下柱(家里) | 这一轮你在这一边动手几次、动作句(同句合并,最多 4 句) | 详情卡:全部动作句与时间、答了哪个问题、指向哪件事 | 沿柱的方向离开基线 8 px |
- 其余怎么变。 悬停时其余降到七成;展开时降到 18 %。悬停条上的一件,回应柱里属于它的那一截保持满色,其余截降下去:看得见你在哪几轮碰过它。悬停一轮,那一轮的竖带浅灰衬底。
- 节拍。 停在一轮中途时,卡守这一轮的节拍:落定之前写“本轮还没落定”,不提前说出后值。
- “去回答 →”。 只给当前查看那一帧仍开着的问题(
askLife[stage.index].open,含子层),链到同一帧的 D(../../index.html?variant=d&frame=<序号>,经withDataset带上?data=);已经关上的写它怎么关上的(答掉写你的动作句,失效写“随改写失效”),不给链接。 - 时序与收回。 点击弹出 8 px(0–0.3 秒)、引线 0.4–0.6 秒、详情卡 0.6–0.9 秒;收回卡 0.2 秒、引线 0.15 秒、落回 0.2 秒;Esc、点空白、再点同一件收回;Tab 到隐藏按钮看概要,Enter 展开、焦点进卡,Esc 回到按钮;↑/↓ 在条上换段;触屏第一下悬停、第二下展开;播放(讲述、连播)时悬停停表,离开 0.4 秒后接着走;←/→、选帧、播放与合页顶栏选帧立即收起。
prefers-reduced-motion时直接到终态。 - 钩子。
window.foldTug照foldYield:...ix.hooks(),加hover / open / pointOf / nearOf(名字可用条目 id 或短名)、pick()(此刻条上最大的一段)、tell / skip / tellAt(秒) / play / stop / telling / playing / settled / phase、rows、plan、perf / resetPerf;window.foldSketch照旧是stage.mjs的舞台。
5 实现与出入
文件。 sketches/tug.html 只链样式与模块;tug.mjs 是全部推导与绘制,tug.css 是纸色主题与 ix 卡的变量覆盖。页框取 stage.mjs(顶栏、侧栏、图例、?frame=、合页同步),stage.play / pause / toggle 换成本页自己的时钟(连播半速,一轮 0.75 秒,不用 stage 的 1.5 秒定时器);页头的小字改成“第四轮 · 关系的画法”。
画法。 一张 SVG 画全部信息层:章节带、网格与持平线、墨面与浅面(两张同一路径、各裁到持平线一侧)、留白刻、注、回应柱、日轴、此刻线、条与条旁的名字;只在尺寸变时重建。逐帧只改条上每段的 y / height、名字的 y、曲线的 d、回应柱的 transform、此刻线的位置与英雄数字,且每个属性带缓存,值不变就不写:静止时本页不写 DOM(见第 6 节)。概念层是叠在条上的一张 canvas(条宽 + 4 px),只画这一轮换手的格子;每格什么时候被分界扫过在过账开始时按 1/240 秒采样一次,所以讲述的任意一刻(tellAt)都画得出同一张。
布局。 1440×900 下图宽约 1052 px:英雄数字块高 98 px,条在 x = 40,宽 40,一份 u ≈ 7.5 px(按图高现算,4.5–9 px),条高 64u;曲线从 x = 258 到右边,一轮一步(composite 约 28 px);持平线 = 条的第 32 格;回应柱一次 k = clamp(50 / 单轮最多次数, 3.5, 9) px(composite 3.85 px,story 9 px)。
和第 1–4 节的出入。
- 留白刻只画留白 ≥ 10 份的轮(草图的门槛)。按 ≥ 1 份画时,story 冷启动后十几轮留白都大,曲线上全是斜线柱,盖住了“工作占大头”。条上的斜线照旧一格一份。
- 曲线上工作领先的一面是浅灰(
--faint),不是纸白。 story 两周大半是工作领先,纸白在纸上看不出面,只剩一条线;浅灰仍比条上的“白”浅一档,读法不变:墨面向下是家里,浅面向上是工作。 - 段的引线从条旁名字的末端出(名字没画时从段的右端),不从段的右端穿过名字;水平出到卡,卡在名字列右边。
- 注的摆位:先摆极值,再摆“对不上”的注,最后摆大挪动;每条先试居中、再试靠左、靠右,再上下让开 13 px;打了点的轮也算障碍。composite 的 f18“向家里 28”因此靠左,不压 f20 的极值点。
- 英雄数字 54 px(第 3 节写的 56 px)。快轮里写成灰的;
tellAt停在快轮上时也是灰的。 - 回应柱按事项切成几截,同一件事的截在条的顺序里叠;静止时不画分隔(一根实柱),悬停条上的一件时它的截描墨边,看得见你在哪几轮碰过它。边用
crispEdges,压暗时截与截之间不露缝。 - 此刻线压在回应柱与注的下面:否则点状虚线把这一轮的柱子切成条纹。
- 跳过讲述:任意键(Tab 跳过但照常移焦点)或在页上按一下(「▶ 讲一遍」按钮本身除外);讲述中按钮写“跳过 ▸▸”。
- 默认帧:catalog 写
frame: 'f26'(composite)。story 直接打开停在 f19(讲述的终点),画廊卡的“默认帧”按sketchFrames或frame显示 f26,与页面不一致;改它要动画廊的逻辑,没有改。 - story 的批注记号 ✎ 有三个:f14(第 3 节写的)之外,f03(10/12 20:30“这不是普通周末待办……”)与 f33(10/24 20:00“别再招一个只接需求的人……”)也有林苒的批注,同样画在那一轮的柱顶;讲述停在 f19,f33 的只在往后逐帧时看到。
时间常数(FULL、SCALE、SWEEP 在 tug.mjs 顶上一处):全速 1.5 秒(拉 0.10–0.70,柱 0.70–1.00),半速 × 0.5,快轮 × 0.3,←/→ 逐帧 × 1/3(0.5 秒);一次跳几轮用 0.42 秒的扫(ease-in-out,不过冲),往回退直接切;换手格子灰 0.28 秒(× 速度,至少 0.12 秒)。讲述 composite 23.05 秒、story 17.3 秒(foldTug.length())。
6 验证
2026-10-28,本机无头 Chrome,静态服务是自起的 node 服务(深队列,不用 4173);机器同时跑着别的代理,负载均值 30–150。
| 项 | 结果 |
|---|---|
make check | 通过 |
node scripts/check-ix.mjs(全部页面,两套数据) | rebase 到 233ae85(含 04 变形、07 峰值保持、03 驻龄)后两套数据各 16 页,拔河 16 / 16;notation 在两套数据并行跑时各有一项“点击展开”卡在展开动画中途,单独重跑两套都 16 / 16。之前负载最高时 yield 也这样卡过一次,单独重跑通过;都不是本页。再 rebase 到 a97ccb8(加了 05 等待链)后重跑 tug、chain、home:两套数据 16 / 16、16 / 16、23 / 23 |
node scripts/shoot-forms.mjs --only tug,all --gpu --check all-sync(两套数据) | 无报错;合页逐节同步到所选帧 |
npm run site:build | 通过(chunk 大小的提示是原有的) |
帧率(?perf、--gpu,foldTug.perf()) | 逐帧 ← → 60 fps(p95 16.7 ms,脚本 p95 0.9 ms);一次跳 23 轮的扫 60 fps;连播 60 fps;composite 讲一遍 1417 帧 60 fps,story 讲一遍 1068 帧 60 fps(脚本 p95 0.7–0.8 ms) |
| 静止为零 | 讲完与带 ?frame= 打开后 2 秒:canvas 没有一个像素;本页的绘制不写 DOM。唯一的写入是 ix.mjs 每帧把概要引线的 d 置空(每秒 60 次,让位图同样,不改画面) |
| 只讲一次 | 顶层不带 ?frame=:讲;任意键与点一下跳到 f26 落定;「▶ 讲一遍」重演、按钮写“跳过 ▸▸”;带 ?frame=、从画廊链来(有 referrer)、嵌在合页与画廊里:不讲;prefers-reduced-motion:直接停在 f26 |
| 数字 | 页面现算与第 2 节一致:composite 讲 23.05 秒,极值 f20(59 : 5),对不上 f08(13 − 1),大挪动 f03、f18、f21、f23;story 讲 17.3 秒,停在 f19(18 : 37),极值 f23,大挪动 f01、f15、f19、f31,没有“对不上”的注 |
静帧(1440×900,--gpu,scratchpad case-tug/ship/,不入库):换轮中途(f23 拉到 0.35 秒,换手的格子在灰)、回应落地(f08 0.85 秒,柱长到 87 %,注已出)、悬停(f26 主水管)、展开到 0.9 秒(f26 主水管),另有 story 的 f15 换轮中途与 f19 终态。