这是公开教材的架构真源。开源仓库保存框架、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 只实现 setView、hydrate、update、dehydrate、draw 和 destroy。销毁 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、绝对路径和凭据;
- 发布前同时执行公开泄漏扫描和私有参考代码相似度门禁。
公开课演示主线
- 先用 Camera 说明 World 与屏幕坐标不是一回事;
- 展示 1,200 个 Document 节点,但只 hydrate 当前区域;
- 展示 ResourcePool 在活跃 lease 下拒绝突破硬预算;
- 让宿主 Agent 通过校验方法一次提交多条 Command,并整体 Undo;
- 在不改 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 框架。