React Canvas 创意编辑器:状态模型比画布更早决定体验
做创意编辑器时,很多人先盯着 Canvas、拖拽、缩放和动画。它们当然重要,但真正决定体验的是状态模型。画布上每一个元素、选择状态、撤销历史、对齐参考线、导出配置,都要有稳定的数据结构支撑。否则界面越漂亮,后期越难维护。
React 做 Canvas 编辑器,最难的不是把矩形画出来,而是让 UI 状态、渲染状态和历史状态保持一致。独立开发者尤其需要控制复杂度,因为一个人维护不了一套失控的编辑器内核。
一、先拆分文档状态和交互状态
文档状态是作品本身:画布尺寸、图层、元素属性、文本内容。交互状态是用户此刻正在做什么:选中了谁、鼠标在哪、是否正在拖拽、缩放比例是多少。两者不要混在一起。
flowchart TD A[Editor State] --> B[Document State] A --> C[Interaction State] B --> D[Layers] B --> E[Elements] C --> F[Selection] C --> G[Drag Session] C --> H[Viewport]如果把isDragging写进元素本身,撤销历史会变脏;如果把 viewport 缩放写进文档,导出结果会被交互影响。边界清楚,编辑器才会稳定。
二、元素模型要保持简单
创意工具可以很美,但底层数据模型要朴素。每个元素至少有 id、type、位置、尺寸、样式和内容。复杂能力可以通过扩展字段增加,不要一开始设计成庞大的继承体系。
type EditorElement = | { id: string; type: "text"; x: number; y: number; width: number; height: number; text: string; style: TextStyle; } | { id: string; type: "image"; x: number; y: number; width: number; height: number; src: string; opacity: number; };这种 union type 比一个万能对象更可读。渲染、属性面板和导出逻辑都可以按 type 分支处理。独立产品最怕“为了未来扩展”提前做抽象,最后未来没来,复杂度先到了。
三、撤销历史只记录文档变化
撤销是编辑器的信任基础。用户敢探索,是因为知道可以退回去。撤销历史应只记录文档变化,不记录鼠标位置、hover 状态和临时参考线。
type HistoryState = { past: DocumentState[]; present: DocumentState; future: DocumentState[]; }; function commit(next: DocumentState) { setHistory((h) => ({ past: [...h.past, h.present], present: next, future: [], })); }真实项目里可以做 patch history,避免大文档复制过多。但早期产品不要急着复杂化。先把语义做对:哪些动作进入历史,哪些动作只是交互过程。
四、性能优化从渲染边界开始
Canvas 编辑器的性能问题,常常不是 Canvas 慢,而是 React 状态更新范围太大。鼠标移动时如果整个属性面板、图层列表、工具栏都重新渲染,拖拽会发黏。
可以把高频交互状态放在 ref 或外部 store 中,只在提交时更新 React 文档状态。画布渲染层也可以按需重绘,而不是每次 state 变化都全量绘制。
const dragRef = useRef<DragSession | null>(null); function onPointerMove(e: PointerEvent) { if (!dragRef.current) return; updateCanvasPreview(dragRef.current, e); } function onPointerUp() { const nextDocument = buildCommittedDocument(); commit(nextDocument); dragRef.current = null; }这个模式让拖拽过程轻,提交结果稳。用户感受到的是顺滑,代码得到的是边界。
五、总结
React Canvas 创意编辑器的体验,早在画布绘制之前就被状态模型决定。文档状态和交互状态要分离,元素模型要简单,撤销历史要可信,高频交互要控制渲染边界。
创意工具的温柔,不是界面上有多少动效,而是用户每一次尝试都被稳稳接住。