使用 react-canvas 制作一个 Figma 工具:从画布到编辑器
使用 react-canvas 制作一个 Figma 工具:从画布到编辑器
如果你想做一个类似 Figma 的设计工具,第一反应往往是:
- 要有高性能画布渲染
- 要有可组合的 UI 结构
- 要有事件命中、选中框、拖拽缩放、文本编辑
- 还要能接入 AI(生成、改图、改布局)
我这次在 apps/open-canvas-lab 里给出的思路是:
用 react-canvas 作为渲染与交互底座,逐步搭一个“Figma 工具内核”。
![]()
项目地址
- GitHub 仓库:github.com/ouzhou/reac…
- 在线预览:open-canvas-lab.vercel.app/
为什么选 react-canvas
react-canvas 这套能力很适合做设计工具,因为它天然覆盖了编辑器最核心的三层:
- 场景渲染层:CanvasKit + 场景树,支持复杂布局、文本、图片、矢量
- 交互命中层:pick buffer + pointer 事件分发,支持精确命中
- 运行时层:场景节点可增删改查,可做撤销重做、选择态同步
相比“直接裸写 Canvas 2D”,这个方案的关键优势是:
你不是在拼命堆 imperative 绘图代码,而是在维护一套可演进的场景模型。
一个可落地的 Figma 工具架构
建议把应用拆成 4 个子系统:
1) Scene(文档模型)
- 以节点树表达 Frame / Group / Text / Image / Path
- 每个节点有 transform、style、约束信息
- 变更统一走 command(方便 undo/redo)
2) Renderer(渲染与命中)
- 主渲染:
react-canvas场景渲染 - 命中:pick buffer 解析到 nodeId
- 选中态叠加:控制框、锚点、参考线
3) Interaction(编辑器手势)
- pointer down/move/up 组合成 drag / resize / rotate
- 框选、吸附、对齐辅助线
- 多选与分组操作
4) Tooling(工具链)
- 左侧图层树、右侧属性面板
- 顶部工具栏(选择、文本、矩形、钢笔)
- 快捷键系统(复制、粘贴、对齐、撤销重做)
在 open-canvas-lab 的实现路线(推荐)
如果你要从 0 到 1 做出可用 MVP,可以按这个顺序:
-
文档与选中
- 建立 node schema
- 点选节点、高亮边框
-
变换编辑
- 拖拽移动
- 8 点缩放
- 基础旋转
-
文本与图片
- 文本节点样式编辑(字体、字号、行高)
- 图片节点 object-fit / 裁剪
-
编辑器体验
- 框选、多选、组合
- 对齐吸附与辅助线
- 撤销重做 + 操作历史
-
协作与 AI(进阶)
- JSON 文档持久化
- CRDT 协同(多人编辑)
- AI 生成组件/版式并写回场景树
AI 能力该怎么落地(重点)
很多编辑器把 AI 做成“聊天框 + 一键生成图”,但真正可用的 AI 设计工具,关键是:
AI 输出必须是结构化编辑指令,而不是一段不可控文本。
建议把 AI 能力拆成 3 层:
1) 意图层(Prompt / Plan)
- 输入:自然语言需求(如“生成一个电商详情页首屏”)
- 输出:任务计划(页面结构、组件清单、风格约束)
- 形态:可审阅的中间 plan(用户可确认/修改)
这一层不要直接改场景,先做“可解释计划”,能显著降低误生成成本。
2) 工具层(Structured Tools)
给模型的不是“任意写 JSON”,而是明确工具集合,例如:
create_frame({ parentId, x, y, width, height, name })create_text({ parentId, text, style })create_image({ parentId, src, fit })update_style({ nodeId, patch })align_nodes({ nodeIds, mode })
模型只负责“调用工具”,具体执行由编辑器 runtime 保证合法性。
这样能把 AI 变成“受约束的自动化操作员”。
3) 执行层(Command Pipeline)
工具调用最终都转换为 command:
command[] -> validate -> apply -> layout -> render- 全量写入 undo/redo 栈
- 每一步都可回滚、可重放
这保证了 AI 操作和手动操作使用同一条数据通路,不会出现“双系统分叉”。
推荐的 AI 能力清单
在 open-canvas-lab 里,建议优先做这 6 类能力:
-
从描述生成线框
- 输入“做一个登录页”,输出基础布局骨架(frame + text + button)
-
风格迁移
- 对选区做“科技蓝 / 极简黑白 / 品牌色系”重绘(仅改 style,不改结构)
-
批量排版
- 统一间距、字号层级、栅格对齐
-
组件重写
- 例如把“普通卡片”一键转成“带封面 + 标签 + CTA”卡片
-
文案智能填充
- 生成标题、副标题、按钮文案,并支持语气风格切换
-
设计审查(AI Review)
- 检查对齐、对比度、可读性、间距一致性,输出可执行修复建议
一个最小 AI 执行链路(MVP)
可以先实现下面这个闭环:
- 用户输入需求
- 模型输出
tool calls - 前端校验参数(schema)
- 转成 command 执行
- 在画布高亮本次改动节点
- 用户可
accept / undo / retry
这个 MVP 的价值是:
你不需要先做很强的模型能力,就能把“AI 可控编辑”体验跑通。
AI 接入时最容易踩的坑
坑 1:让模型直接返回整份文档 JSON
问题:diff 巨大、不可控、很难回滚。
建议:必须改为“增量工具调用 + command 化执行”。
坑 2:AI 操作绕开编辑器状态机
问题:会破坏选中态、历史栈、约束关系。
建议:AI 与用户操作走同一 command pipeline。
坑 3:没有失败兜底
问题:工具半执行状态下文档损坏。
建议:每批 AI 操作做事务边界(失败整体回滚)。
坑 4:可解释性不足
问题:用户不知道 AI 改了什么。
建议:展示“本次修改节点清单 + 属性 diff”。
JSON 设计(简版)
为了让 AI、编辑器、存储三方都能稳定协作,建议把 JSON 拆成两层:
- document schema:描述页面与节点树(可持久化)
- command schema:描述一次编辑动作(可回放、可撤销)
1) document schema 示例
{
"version": "1.0",
"meta": { "name": "Landing Page", "updatedAt": 1776259200000 },
"rootId": "frame_root",
"nodes": {
"frame_root": {
"id": "frame_root",
"type": "frame",
"name": "Page",
"children": ["title_1", "btn_1"],
"layout": { "x": 0, "y": 0, "width": 1440, "height": 900 },
"style": { "backgroundColor": "#ffffff" }
},
"title_1": {
"id": "title_1",
"type": "text",
"text": "Build with react-canvas",
"layout": { "x": 120, "y": 160, "width": 600, "height": 72 },
"style": { "fontSize": 56, "fontWeight": 700, "color": "#111827" }
},
"btn_1": {
"id": "btn_1",
"type": "frame",
"name": "CTA",
"children": [],
"layout": { "x": 120, "y": 280, "width": 168, "height": 48 },
"style": { "borderRadius": 12, "backgroundColor": "#2563eb" }
}
}
}
2) command schema 示例
{
"id": "cmd_20260415_001",
"type": "update_style",
"payload": {
"nodeId": "btn_1",
"patch": { "backgroundColor": "#1d4ed8", "borderRadius": 14 }
},
"meta": { "source": "ai", "traceId": "run_xxx" }
}
这套拆分的好处是:
- 文档 JSON 负责“当前状态”
- command JSON 负责“如何到达这个状态”
- AI 输出 command,比直接覆盖整份 document 更安全
关键实现细节(踩坑重点)
坐标系统一
编辑器里至少有 3 套坐标:
- 视口(client)
- 画布(stage)
- 节点局部(local)
一定要优先统一坐标映射,否则拖拽、选框和命中会经常“看起来差几像素但很难查”。
命中与视觉分离
不要用“可见像素”直接做命中判断。
正确姿势是用独立 pick 语义层(nodeId 编码),可维护性和稳定性会高很多。
编辑器状态尽量事件化
把“鼠标按下后进入哪种模式”建成有限状态机(FSM),比 scattered boolean 更稳,后续加钢笔、裁剪工具也不容易崩。
一个简单但重要的结论
做 Figma 工具真正困难的不是“画出来”,而是:
- 模型是否可持续演进
- 交互是否可组合
- 渲染/命中/状态是否解耦
react-canvas 的价值在于,它已经把底层最难啃的部分(渲染与交互基础设施)提前搭好。
你可以把主要精力放在“产品能力”和“编辑体验”上。
结语
如果你正在基于 apps/open-canvas-lab 做编辑器方向的实验,这个方向是可行的:
先做一个“可编辑画板 MVP”,再逐步补齐 Figma 级能力,而不是一上来追求完整复刻。