这是公开教材的架构真源。开源仓库保存框架、Demo 和构建规则,不再手工维护同一篇教材正文。

结论

没有一个现成项目能同时提供“框架无关无限画布、有界资源缓存、宿主 Agent 的事务化接入、可公开授课教材”四项能力。本课程采用组合式架构:

  • 教材组织参考 Infinite Canvas Tutorial 的“文章与运行效果同页”;
  • 网站使用 Astro,直接从外部 Markdown 真源构建;
  • Canvas2D 是零框架基线,Konva 是可选 Renderer Adapter;
  • Document 是作品真相,Renderer 和已解码资源都可以释放与重建;
  • 开源包不内置 Agent;宿主 Agent 只能通过类型化 Command 和同一事务入口修改 Document;
  • 教材只能通过白名单 demo 标识引用仓库内已审查 Demo,不能注入脚本。

先建立一张完整心智图

这套课程不是在教“如何调用 Canvas API 画矩形”,而是在回答五个相互关联的问题:

坐标问题:作品怎样向四周延伸,但屏幕永远有限?
容量问题:Document 很大时,为什么当前对象数量仍能有界?
资源问题:图片和视频离屏后,怎样真正释放解码与播放器成本?
编辑问题:用户、脚本和 Agent 怎样共用可撤销、可审计的写入口?
发布问题:怎样把原理开源,同时不泄漏业务代码与凭据?

可以用“目录、仓库、上架单、展台、操作单”记住核心角色:

架构概念 类比 回答的问题
Document 商品目录 整幅作品里有什么
AssetStore 素材仓库 图片、视频和 LOD 在哪里
SpatialIndex 仓库位置索引 Camera 附近可能有什么
Working Set 当前上架单 现在需要为谁付出成本
Renderer 真实展台 当前怎样显示和交互
CommandBus 受控操作台 谁能怎样修改作品
History/Persistence 操作记录与长期存档 如何撤销、崩溃恢复和重启恢复

最重要的分离是:

Document 很大
  ≠ Working Set 必须很大
  ≠ Renderer 必须有同样数量的对象
  ≠ 全部媒体必须同时解码

如果这四个数量仍然绑定,系统只是“看起来可以无限平移”,并没有真正解决容量问题。

开源方案对比

项目 最值得学习 不直接作为底座的原因
Infinite Canvas Tutorial 每课独立目标、Markdown 与运行结果同页、整站部署 主线偏渲染引擎,不覆盖大文档工作集、资源预算与 Agent 事务
Genji Observable 风格的响应式代码单元 服务端生成 Markdown 时,任意代码执行会扩大攻击面
tldraw Agent Starter Kit 分层上下文、类型化 Action、反馈循环 当前生产使用有额外许可要求,且 Core 与特定 UI 技术栈绑定
OpenPencil 文档是真相,UI、CLI、MCP 共用操作内核 是完整设计编辑器,不是小型教学内核
Excalidraw MIT、开放元素格式、Canvas2D 渲染 是完整 React 应用,资源预算与框架无关 Agent 不是主线
Konva MIT、场景节点、交互能力与丰富 Demo 场景树不能代替可持久化 Document,也不自动提供工作集和资源预算

调研时固定查看 infinite-canvas-tutorial 提交 c8706dbd5edb174570f627d299a0428d36a60b6f。课程借鉴教学组织,不复制其实现。

为什么采用组合式而不是直接二次开发

开源项目各自优化的目标不同:

  • tldraw 的 Store、空间索引、Asset 与增量 History 很适合学习数据层,但它同时拥有完整 Editor、Camera 和 UI;
  • Excalidraw 的双层 Canvas2D、开放元素格式和交互体验优秀,但固定版本的视口过滤仍需要扫描 Scene,图片缓存也不等于资源预算;
  • Konva 适合作为当前产品的场景图与交互层,但默认场景树不会替开发者管理大文档 Working Set、LOD 和持久化;
  • Infinite Canvas Tutorial 很适合组织“原理文章 + 同页 Demo”,但课程目标偏渲染引擎;
  • Agent Starter Kit、OpenPencil、Excalidraw MCP 分别证明了分层上下文、统一工具注册表和检查点续改,但都不能直接变成任意旧项目的通用 Agent 插件。

所以本方案抽取的是共同边界,而不是复制某个产品:

Framework-neutral Core
  + Canvas2D/Konva Renderer Adapter
  + Browser/Host Persistence Adapter
  + Host-owned Agent Planner and Policy

这也解释了“框架无关”的准确含义:Core 不依赖某个 Renderer 私有类型,但 Adapter 内部仍然可以充分使用 Konva 或浏览器 API。

总体架构

flowchart TB
    Human["鼠标 / 键盘 / 属性面板"] --> Bus["CommandBus"]
    Agent["宿主 AI / 规则 / 人工流程"] --> Policy["宿主 Policy + 框架校验方法"]
    Policy --> Plan["AgentPlan 数据"]
    Plan --> Bus
    Bus --> Doc["Canvas Document"]
    Doc --> Index["SpatialIndex"]
    Camera["Camera + Viewport"] --> Bounds["Visible World Bounds"]
    Bounds --> Index
    Index --> Set["Working Set"]
    Set --> Renderer["RendererPort"]
    Renderer --> Canvas2D["Canvas2D Adapter"]
    Renderer --> Konva["Konva Adapter"]
    Set --> Pool["ResourcePool + LOD"]
    Doc --> Persist["Persistence Port"]
    Assets["AssetStore"] --> Pool

核心因果链只有一条:

Camera
  -> Visible World Bounds
  -> SpatialIndex.search
  -> Working Set
  -> hydrate / stay / dehydrate
  -> Renderer draw

但运行时实际上有两条必须分开的主链。

高频视图链

Camera / Viewport 变化
  → 计算 Visible 与 Overscan World Bounds
  → SpatialIndex.search
  → Working Set diff
  → enter / stay / exit
  → Renderer draw

这条链可能每帧发生,不能等待持久化、生成 LOD、复制完整 Document 或调用远程 Agent。

低频作品编辑链

Pointer / Panel / Agent 意图
  → CanvasCommand[]
  → Transaction 校验
  → Document revision
  → 增量更新 SpatialIndex
  → History / Journal / Renderer

Camera 移动只走视图链;节点移动才进入作品编辑链。把两条链混在一起,会产生全量节点改写、History 污染和持久化抖动。

五条不变量

1. Document 是作品真相

Document 保存稳定 ID、节点类型、World Bounds、层级、资产引用和可序列化属性。它不保存 Konva Node、DOM、ImageBitmap、Object URL、播放器或 GPU Texture。

2. Renderer 是可丢弃投影

Renderer 只实现 setViewhydrateupdatedehydratedrawdestroy。销毁 Canvas2D 或 Konva Adapter 后,使用同一 Document 应能恢复相同作品。

3. Working Set 必须有界

大文档不等于全量挂载。空间索引从视口和 overscan 推导候选节点,交互中的 pinned 节点不会在拖拽途中被释放,exit grace 用于吸收边界抖动。

4. 原始资产和运行时资源分开治理

作品仍引用的 original 不能按普通 LRU 删除;thumbnail、LOD、decoded bitmap、texture 和 player 可以重建,应受字节数、对象数和租约约束。

5. 人与 Agent 共用事务入口

鼠标、属性面板和宿主 Agent 最终都生成 CanvasCommand[]。事务校验 revision,记录 before/after,支持幂等 actionId、整体 Undo/Redo,并在失败时保持原子性。框架只提供方法和约定,模型、提示词、密钥、节点类型和业务校验都属于引入方。

三个预算共同决定系统上限

“只渲染可见节点”仍然不够。完整容量需要三层预算:

节点预算
  Working Set / Renderer Handle / Hydrate Queue

媒体预算
  Decoded Bytes / Decoded Objects / Active Players / In-flight Decode

历史预算
  Transaction Steps / History Bytes / Journal / Checkpoint

三层分别防止场景对象、媒体内存和撤销数据无界增长。预算不足时应降级或拒绝请求,不能通过驱逐活跃资源、丢失 Document 或破坏 Undo 来“强行成功”。

一次 Camera 平移的完整原理

假设 Viewport 是 1200 × 800 CSS 像素,Camera zoom 为 2,overscan 为 240px:

可见 World 宽高 = 1200/2 × 800/2 = 600 × 400
overscanWorld = 240/2 = 120

Camera 先得到精确 World Bounds,再向四周扩张 120 个世界单位。RBush 查询 Expanded Bounds,Working Set 合并 Pinned 节点,与上一帧做集合差分:

enter → Hydrate,创建 handle 并获取资源 lease
stay  → 复用已有对象,只更新必要属性
exit  → grace 后 Dehydrate,释放运行时资源

Document 节点坐标不因平移变化,SpatialIndex 也不重建。只有查询范围与运行时对象集合改变。

一次 Agent 修改的完整原理

用户输入“把选中的三张卡片水平排列”:

1. Host 构建有界 CanvasAgentContext,带 revision
2. Planner 输出 arrange_nodes(ids) 数据
3. ToolDef 校验 ID、数量和权限
4. 本地布局算法展开为 3 条 update Command
5. CommandBus 校验 expectedRevision 和业务不变量
6. 3 条 change 作为一个 Transaction 原子提交
7. Document/SpatialIndex/Renderer/History 消费同一结果
8. 结构验证和视觉验证分别给出结论

模型不接触 Konva Node,不生成任意 JavaScript,也不拥有 API Key 的存储策略。开源框架提供的是可集成方法,不是内置 Agent 产品。

教材与 Demo 的单一真源边界

Obsidian 中的公开课 Markdown(唯一正文真源)
  -> Astro Content Collection 只读加载
  -> frontmatter.demo 白名单解析
  -> 仓库内已审查 Vue Demo
  -> 静态站点 HTML / JS

仓库不提交课程 Markdown 副本。服务端生成器只能写入显式配置的内容目录,并且:

  • API Key 只存在服务端;
  • 文件名和 slug 必须通过白名单格式校验;
  • demo 必须存在于仓库注册表;
  • 禁止 script、iframe、事件处理器、外部 import、绝对路径和凭据;
  • 发布前同时执行公开泄漏扫描和私有参考代码相似度门禁。

公开课演示主线

  1. 先用 Camera 说明 World 与屏幕坐标不是一回事;
  2. 展示 1,200 个 Document 节点,但只 hydrate 当前区域;
  3. 展示 ResourcePool 在活跃 lease 下拒绝突破硬预算;
  4. 让宿主 Agent 通过校验方法一次提交多条 Command,并整体 Undo;
  5. 在不改 Document、History 和宿主 Agent 的情况下切换 Canvas2D 与 Konva。

这五步分别证明:坐标稳定、容量有界、资源有界、修改可控、渲染框架可替换。

推荐学习顺序与每课证据

课次 核心问题 现场证据
01 World/Camera 节点为什么没随平移改变 坐标正逆变换与可见 Bounds
02 Document/Renderer 为什么 Renderer 可以销毁 切换后 ID、Bounds、History 不变
03 Spatial/Working Set 大文档为什么只挂载少量对象 1,200 节点与有界 handle 计数
04 Hydrate/Dehydrate 离屏后究竟释放什么 grace、release 与重访恢复
05 Cache/LOD 为什么 5MB 图会占几十 MiB LOD 选择、lease 与硬预算拒绝
06 Persistence 刷新恢复与运行时性能有何区别 IndexedDB 恢复后仍只 hydrate 当前区
07 Command/History 为什么一次任务只 Undo 一次 批量事务、冲突和幂等重试
08 Canvas Agent 模型怎样安全修改大画布 有界上下文、Policy 和视觉复核
09 Renderer 框架无关怎样落地 Canvas2D/Konva 真实切换
10 Observability 怎样证明而不是感觉 固定基准、预算、故障和泄漏门禁

完成课程后,读者不只会运行 Demo,还应该能把 Core、Cache 或 Agent 契约逐步接入一个已有项目,而不要求先迁移整个 UI 框架。