前言
前段时间一直在琢磨一个事儿:能不能用自然语言来编辑矢量图形?就是说,我不用去手动拖拽、调参数,直接跟AI说”把这个圆变成红色”、”把所有矩形向右移动50像素”,它就帮我改好了。听起来有点科幻,但实际做下来发现,只要把Prompt工程和画布操作的桥梁搭好,这事儿还真能成。这篇文章就来聊聊这个AI驱动的SVG编辑器是怎么做的。
正文
一、技术栈选型
做一个Web端的SVG编辑器,技术选型是第一步。我最终选定的组合是:
- React 19 + TypeScript:前端框架没什么好纠结的,React生态成熟,TypeScript的类型安全在这种复杂交互的项目里几乎是必须的。编辑器里各种对象属性、AI响应结构、Patch操作,没有类型约束的话很容易写出bug。
- Fabric.js 5:这是核心中的核心。Fabric.js是一个强大的Canvas渲染引擎,它把HTML5 Canvas封装成了面向对象的API——每个图形元素都是一个JavaScript对象,有自己的属性(位置、颜色、旋转角度等),可以被选中、拖拽、变换。选它而不是直接操作SVG DOM,是因为Fabric.js提供了开箱即用的交互能力(选择、缩放、旋转手柄),省了大量的底层工作。
- Zustand:状态管理用的Zustand,比Redux轻量太多了。编辑器的状态其实挺复杂的——画布上有哪些对象、当前选中了什么、AI正在处理什么请求、undo/redo栈里有什么——Zustand用起来就像写普通的JavaScript对象一样直觉,没有Redux那套繁琐的action/reducer仪式。
- Radix UI + Tailwind CSS:UI组件和样式方案。Radix UI的无障碍支持做得很好,Tailwind写样式快。
二、AI编辑的三种模式
AI编辑不是一刀切的,不同的操作场景需要不同的”权限范围”。我设计了三种编辑模式:
Global模式(全局)
这是权限最大的模式。AI可以看到画布上的所有对象,可以修改任何对象、添加新对象、删除对象。适合那种”大刀阔斧”的操作,比如”给画布添加一个标题”、”把所有矩形的颜色统一成蓝色”。在这个模式下,发给AI的上下文(Context)包含整个画布的SVG内容。
Selection模式(选区)
当你在画布上选中了多个对象时,自动切换到这个模式。AI只能操作你选中的那些对象,可以修改它们的属性,也可以在它们附近添加新对象,但不能动画布上其他的东西。比如”把选中的这三个形状对齐”、”给选中的元素统一加个阴影”。上下文只包含选中对象的信息。
Element模式(单元素)
选中单个对象时进入这个模式,权限最小。AI只能修改这一个对象的属性,不能添加也不能删除。比如”把这个圆的半径改成100”、”旋转45度”。上下文只有这一个对象的SVG。
这三种模式的切换是自动的——根据画布上的选择状态来判断。这样做的好处是,AI的”活动范围”被精确控制了,不会出现你只想改一个元素结果AI把整个画布都重排了的情况。同时,发给AI的上下文也更精简,token消耗更少,响应也更快。
三、Prompt工程:让AI理解Fabric.js属性
AI编辑的效果好不好,很大程度上取决于Prompt写得好不好。核心挑战是:让AI知道Fabric.js有哪些属性可以改,每个属性是什么意思,值的范围是什么。
我的做法是在System Prompt里塞了一份”Fabric.js属性速查表”,告诉AI:
- 通用属性:
left、top、angle、scaleX、scaleY、fill、stroke、opacity等 - 矩形特有:
width、height、rx(圆角) - 圆形特有:
radius - 文本特有:
text、fontSize、fontFamily、fontWeight
同时明确告诉AI,它的输出必须是一个JSON格式的Patch数组,每个Patch包含action(modify/add/remove)和对应的参数。这样AI的输出就是结构化的,可以直接被程序解析和执行。
另外还有一个”黑名单”机制——有些属性是绝对不能让AI碰的,比如objectId、type、canvas这些内部属性。如果AI的响应里包含了这些属性,会在应用前被自动过滤掉,防止出现安全问题。
四、Patch系统:AI响应到画布操作的桥梁
Patch系统是整个AI编辑流程的核心枢纽。AI返回的是一组JSON描述的操作指令,Patch系统负责把这些指令”翻译”成实际的画布操作。
三种Patch类型:
- Modify:修改已有对象的属性。指定
objectId和要改的changes,比如{ action: "modify", objectId: "obj_1", changes: { fill: "#ff0000" } }。 - Add:添加新对象。指定
objectType和初始properties,比如{ action: "add", objectType: "rect", properties: { left: 100, top: 100, width: 200, height: 100 } }。 - Remove:删除对象。指定
objectId即可。
Patch的应用流程是这样的:
- 快照保存:在应用任何Patch之前,先把当前画布状态存一份快照。万一出了问题,可以一键回滚。
- 属性清洗:过滤掉黑名单里的危险属性。
- 批量执行:所有Patch被包装成一个
CompositeCommand,作为一个原子操作执行。要么全部成功,要么全部回滚。 - 历史记录:执行成功后,这个CompositeCommand被推入undo/redo栈,支持撤销和重做。
这套设计的好处是,AI编辑和手动编辑共享同一套undo/redo机制。你可以先让AI改一通,觉得不满意就Ctrl+Z撤回,然后手动微调,再Ctrl+Z撤回手动操作,回到AI改之前的状态。整个编辑历史是线性的、可追溯的。
五、Command模式与Undo/Redo
编辑器里有一个很重要的体验问题:AI改完之后不满意怎么办?总不能让用户手动一个个属性改回去吧。所以undo/redo是必须的,而且AI编辑和手动编辑必须共享同一套撤销栈。
我用的是经典的Command模式。每一个操作(不管是手动拖拽、修改属性,还是AI批量编辑)都被封装成一个Command对象,包含execute和undo两个方法。AI编辑产生的多个Patch会被包装成一个CompositeCommand,作为一个原子操作推入历史栈。这样Ctrl+Z一下就能撤回整个AI编辑,而不是只撤回其中一个Patch。
每个画布上的对象都有一个唯一的objectId,通过ensureObjectId机制在对象创建时自动分配。这个ID贯穿了整个编辑流程——AI通过它定位要修改的对象,Command通过它记录操作目标,undo的时候也靠它找回对象。
六、上下文优化:让AI少花钱多办事
调AI API是要花钱的,token数直接决定了成本和响应速度。一个复杂的SVG画布可能有几十个对象,导出的SVG代码轻松超过50000个字符。如果每次都把完整的SVG丢给AI,既浪费token又拖慢响应。
所以我做了几层优化:
- 模式级裁剪:前面说的三种编辑模式,本质上就是上下文裁剪。Element模式只发一个对象的SVG,Selection模式只发选中对象的,Global模式才发全部。大多数编辑操作其实都是针对一两个元素的,这一层就能砍掉大部分token。
- SVG属性精简:当SVG内容超过阈值时,自动裁剪掉那些对AI编辑来说不重要的属性——
transform、filter、clip-path、stroke-dasharray这些渲染细节。AI需要知道的是”这是个红色矩形在坐标(100,200)”,不需要知道它的描边虚线间距是多少。 - 对象摘要:给每个对象生成一个类型标签(rect、circle、text等),让AI快速理解画布上有什么,而不是去解析原始SVG标签。
另外还有一个实用功能:SVG Code Viewer。它能实时显示画布对应的SVG源码,支持语法高亮,而且点击代码里的某个元素标签,画布上对应的对象会被选中高亮。反过来,在画布上选中一个对象,代码视图也会自动滚动到对应位置。这个双向联动在调试AI编辑结果的时候特别好用——你能直观地看到AI到底改了哪些属性。
最后
参考文章:
Zustand - Bear necessities for state management
声明
本文仅作为个人学习记录,由AI辅助编写。