介绍我的 SVG Maker

2025/08/02 | 11分钟阅读 | 更新于 2026/04/28

介绍我的 SVG Maker

简介

项目部署在了 svg-maker.rect.page , 源代码和技术细节可以查看 Github bin16/svg-maker . 开发日志和思考过程在下面,但是应该不太会有人想看这玩意儿吧。

原始更新日志

这个东西最开始叫 SVG Path Builder,后来改名 svg-tools,再后来变更为 svg-maker。

然后今天(2026/04/28),我更新了一个 README 上去。

下面是原始的更新日志,写在我的笔记中的。中间空缺的天数,不是没干活,多半是写的太简略了只有一句「更新 svg-tools」这种东西。

2025-05-07

正在研究 SVG Path builder

目前感觉设计上最喜欢的:

https://easycodetools.com/tool/svg-path-builder

可以拖拽点和控制点,会吸附网格,并且可以随意切换点/命令的类型,保留起点和终点。没有缩放功能,画布也是固定死的。

功能最完善的可能是:

https://yqnn.github.io/svg-path-editor/

然后这个更侧重命令,在选中/悬停在命令上的时候会用虚线标注对应的路径,但是没有网格。

https://svg-path.com/

2025-05-08

更新 SVG Path Builder,实现了拖拽和网格吸附之类的操作,另外稍微整理了一下侧边的 UI。目前大致是一个可用状态,然后删除 command 的操作目前还不是很完善,也没办法创建更多 path,以及缺少保存功能。

有一个大胆的想法,既然多个 path 都在计划中了,那么顺便把 rect / circle 之类的图形也加进来,似乎也是可以接受的,然后就会变成一个简单的 svg 编辑器……

  • 支持获取前一个 command 的 x 和 y,有了这个就可以把 H 和 V 命令加回来,以及可以支持相对坐标,另外就是生成贝塞尔曲线的控制点应该会用得上。

2025-05-09

更新 svg-tools

虽然是 path builder,但是考虑到单个 path 没有多少用处,于是增加了图层 layer 功能,另外在 path 之外,支持了别的图形 rect 和 line。但是一个 layer 只能有一个 shape,所以这里的 layer 只是给 shape 提供额外的元数据。

然后ui也要改,目前完成了大部分工作,但是删除 command 暂时没有了,另外 layer 那边也需要修。

一个设想是,像 Aseprite 那样,再加一个时间轴,实现动画系统。不过这个之后再做,目前只考虑一下有没有必要给它预留数据结构,比如把现在的 layers 放进默认的 frame 里面。

接下来要做的部分是

  • path 支持删除 command
  • layer 支持排序,隐藏,…
  • 当前 command 以及控制点的强调显示
  • 一个简单的颜色编辑器,rgb hex 用滑杆实现;不知道这个能不能做个组件出去

2025-05-10

更新 svg-tools

更新 DragZone,现在拖拽会直接计算相对画布的绝对坐标,而不是像之前那样在旧的坐标的基础上计算差值。好处是要管理的数据变少了,不用保存一份 x/y 和 clientX/clientY 了,直接当鼠标移动的时候拿到 clientX/clientY,和画布的坐标比对,然后转换一下得到实际的 x 和 y 就可以了。而且如果 x/y 没有改变(尤其在开启网格吸附的情况下)就不上报,现在拖拽的连贯性好了很多。

另外原本会存在有时候没有成功释放的情况,控制点依然在跟着鼠标走。修改为在 onDragStart 里面判断,如果已经是拖拽状态了,就改为释放。这样当意外遇到控制点没有释放的情况,只要自然地点一次鼠标,就会重新触发释放操作。

另外写了一个取色器,只有 RGB 三个滑杆以及 HEX 输入,另外有个按钮允许将颜色设置为 none。样式上借用了 DaisyUI 的输入框,背景色改为当前的颜色。 取色器的弹窗是通过 getBoundingClientRect 计算了按钮所在的位置之后计算出来的,然后外层是一个 fixed 的透明遮罩,用于关闭弹窗。

不过 RGB 滑杆不是很好用,并不能直观的预见到自己会搞出一个什么颜色,感觉还是需要一个类似 HSL 或者 HSV 的东西。

一个想法,现在也许可以塞一个 iro.js 在弹窗里。 https://iro.js.org/

2025-05-13

更新 svg-tools

因为各种东西都放在 route 里面,导致不方便使用绝对路径/别名,看上去非常混乱,于是进行了一系列重构。

Layer 更名为 Shape,不过有点分不清 Shape 还是 Figure。

创建了一系列新的组件,尝试改进设计。

创建和更新类的写操作,现在改成使用自定义的 hooks,于是就不需要创建那么多 atom。

2025-05-14

更新 svg-tools

感觉前一个版本的 path - commands 结构不是很合理,于是又搞了一个新版本的 path-v2。

给 path commands 令分了组,每一个 M 都是一个新的 group,group commands 只保留 L/C/S/Q/T/A,M 直接作为 group 本身,确信来说是 group.head,Z 在 group 上是一个 boolean 属性。

有很多地方要改动,写起来好麻烦。

immer 在很多地方都很好用,可以像立即执行函数那样隔离内外作用域,另外就是写 map 之类的纯函数的时候写法可以更自由。

2025-05-16

首先是 path 支持了 relative 命令。

然后修复了 path 在连续的 S 或者 T 命令的时候 segment 会错位的问题,问题原因是计算控制点的时候需要用到前一个点的 x0 y0,于是重新写了一下这部分流程,先随便填一个值进来,完成扩展之后再进行计算。

然后把能加进来的图形都加进来了,polygon, polyline, circle, ellipse, line。

尝试移除 path v1 以及 route/ 下面的陈旧组件。计划重构 path v2 为新的 path,有一些 id / 状态的东西需要好好考虑清楚。

2025-05-17

更新 svg-tools,今天主要是解决了 Shape 和 Figure 的混用问题,进行了一些清理工作。另外重新整理了 path-v2 这边 utils 的名字(即使这样依然不是很清晰)。

改了 PathV2 的结构,原本想要去掉 group.head 这一层,但是考虑再三,最终选择把 head 改名为 move,这样就清晰了一些。

2025-05-19

今天也写了一些 svg-tools,主要是样式上的改动,新的 widget 样式加上 PopOver / DropDown 让 UI 整体上变简洁了不少。然后把 Arc 命令里的两个 Switch 换成自己写的 Swap,直接用图标来表示值,命令看起来短了不少。

试了一下自己改了 24x24 的 viewBox,配合 scale(应该改名 zoom),画了两个图标表示 largeArcFlag 的状态。注意到 lucide icons 的画布也是 24x24,很多坐标用的是整数。大概 24 这个数字有很多小的因子,比较容易等分,就类似 bootstrap 最开始的12列的布局。

接下来要做的,新建和转换 path command 的逻辑需要统一;widget 的样式需要简化,增加更多修饰符给不同的场景使用,然后属性编辑器的布局感觉也需要调整。保存功能。

2025-05-21

今天写了一点点代码,是借助 idb 以及 hooks,简单测试了一下 svg-tools 新建文件和保存功能。可以在 hook 里面用 state 管理登录状态,useEffect 触发请求。把所有的东西一起返回,页面就可以根据打包好的数据展示对应的状态,hooks 还是很方便的。

2025-05-22

更新 svg-tools ,完成了文件读写,增加了一个最近文件的列表页,一个创建 svg 选择画布预设的页面。然后试了一下参考 lucide 创建了几个 24x24 的 svg icons。

2025-05-23

DEV/svg-tools 调整了一下细节,像是创建 path command 的时候,修复了控制点会跑到画布外的 bug,另外创建或者转换为 A 命令的时候,会直接使用 dx / dy(的绝对值)作为 rx 和 ry。在用 L 命令画大形,然后把拐角转成 A 命令的时候尤其有用,不需要手动调整半径。

2025-05-24

更新 #DEV/svg-tools

做了一些 UI 上的改进,控制点现在有三种形状,circle 作为「起始点」,正方形作为 path / polygon / polyline 的其它的点,菱形作为曲线的控制点。

增加了 StyleForm,用于替代旧的 StyleInfo,更多的使用了原生的组件,然后支持重置属性为默认值。另外发现 lucide icons 是直接在 svg 上定义的 stroke 之类的样式,于是在自己的项目里也实现了全局样式。

增加和替换了大量的 icons,其中 lucide 并没有合适的「椭圆」和「直线(段)」的 icon,于是自己画了两个。


#TODO

现在创建 shape 要点击 New Shape 按钮,然后会半随机地在画布上生成图形,然后再去修改,感觉不是很优雅。

  • 考虑在左边增加一个工具条,直接点击图形对应的图标,进入创建流程。然后在画布上选点,连线……
  • 某种移动工具,用于移动整个 shape 或者所有的 shape
  • 把拖拽操作更换为两次点击,这样比较省手
  • Cmd/Ctrl + 点击,直接在画布选择 shape
  • 一个 objectsAtom / useObjects,以及支持 group
  • 考虑到现在有了全局样式,还计划将来增加 group,group 也可以有样式,那么可能不需要给 path 分组了。也许可以隐藏新建分组的功能。
  • path v2 重命名

2025-05-26

更新 svg-tools 今天在思考用户直接选择工具在画布上把图形「画」出来的实现方案,显而易见的之前的 DragZone 太局限了,不好扩展。

注意到不同工具和阶段的状态是有限的,考虑能否用 XState 来创建状态机来管理状态。不过 XState 的类型定义方式实在是太不优雅了,并且有点难以想象用 XState 来管理那么复杂的状态。于是咨询了 DeepSeek,它建议说可以拆分成多个 machine 然后 invoke,不过这时候我突然意识到,那我也完全可以用几个 atom 存储正在编辑的数据和状态,用一些派生 atom 来分别处理不同状态的事件输入的逻辑,如果复杂的话还可以用 reducer。

DeepSeek 给了一个方案,晚上在画状态图的时候想到说,我可以给每个 shape tool 各定义一个 data atom 和 stage atom 分别存储数据和状态。创建和编辑都使用同一组 atom 管理,创建(选点)以及移动和缩放都是同一个工具(状态机)的状态切换。

然后上层搞一个 toolAtom 存储工具名称或者 none。用户新建图形就更新 toolAtom,给对应的 data atom 写初始值,然后更新 stage atom 的状态为创建(确切来说是选择第一个点)。如果是编辑同样更新 toolAtom,把图形数据写到 data atom,同样更新 stage atom 为编辑。DeepSeek 给出了积极的评价,并建议使用 atomFamily 来做中转,另外建议坐标更新使用防抖原子。

const debouncedPosAtom = atomWithDebounce(
  rawPosAtom,
  50 // 50ms防抖
)

我喜欢最终的方案,期待实现。

#DEV/svg-tools #TODO

2025-05-26 (2)

另外我还想了一下坐标超出画布的问题,没关系,有 view-box,给 x y width height 加一点 padding,就可以多出一块空间。然后只要在中间画个框标记最终的显示区域,问题就解决了。

可以让用户选择保留多大的「出血空间」好像是这么翻译。

2025-05-27

#DEV/svg-tools

更新 svg-tools,实现了昨天的新设计方案,现在结构清晰了很多。

创建或者编辑已有的 shape,会通过 tool/shape.ts 「加载」数据,然后通过 ShapeToolHelper 展示预览状态,以及处理拖拽输入的逻辑。每个 shape tool 之间各自独立,加上 ts-pattern 的 match with。

不过 path 这边复杂好多,现在的新方案是:

首先,stage 这边有 +L +M 之类的状态,用于创建对应的 command;然后还会有 move-point / move-p1 / move-p2 之类的,用于移动点/控制点。另外会存一个 index,有了它就能知道是哪个 command 正在创建/编辑,加上前面的 move-point,就可以实现编辑操作。


修复了一个 bug,circle 和 ellipse 会在超出范围后疯狂膨胀,原因是事件绑定放在 g 上面了,g 的大小不是固定的,导致图形超过画布范围时,g 会变大,然后 x 和 y 会变成负值,导致计算得到的相对左上角的坐标变得特别大。

2025-05-28

更新 DEV/svg-tools ,实现了计划中的 path。通过为每一种 command 各自创建 Segments 和 Controllers 两种 helper,现在不需要像以前那样维持一大堆 hook 了。

然后清理了一下 path-v2 的逻辑,去掉了创建和修改相关的功能,只保留查看功能,作为兼容的需要。

不过目前有一个不是问题的问题,现在创建 shape 的逻辑是在 tool 这边的,然后通过 effect 更新的 shapesAtom。但是如果 id 不存在,就会多创建一个 shape,这个问题会出现在从一个文件离开进入另一个文件的时候,理论上可以加一些清理的工作,不过应该有更合理的方案。

首先是 effect 这边,似乎没有必要保留 effect,

2025-05-29

部分实现了 group 相关的功能。最终还是使用了 groupId 这种平坦的结构,通过 buildTree 来构建成树。

然后参考了 Inkscape 的一些设计。

#TODO

  • activeShapeId 可以是 shape 也可以是 group id,因为的确有选中 group 的需要;相应的,group 有两种选中状态,一种是直接选中,一种是间接选中,activeGroupId 是一致的,不影响在这个 group 下面创建 shape;但是,group 的背景色应该是不同的。
  • 目前 selectTool 的逻辑太奇怪了,需要重新拆分 createShape 以及 loadShape 之类的逻辑。创建 shape 的时候应该允许传一些值过来,比如 groupId,考虑在这个时候就直接把 shape 放到 shapesAtom 那边。
  • 一些统计功能,用来生成 shape name
  • 排序 weight 目前没有使用,需要确认
  • 拖拽排序/移动分组

#DEV/svg-tools

2025-05-31

更新 #DEV/svg-tools

完全放弃 dnd-kit,直接使用原生的 drag api 实现了一个可以拖拽排序的树状的列表。借鉴了 dnd-kit 的示例的样式设计,使用 -1px 的 margin 「合并」了重复的边框。圆角改成 xs 之后错落有致还蛮好看。

一个思路是,对 group 这种比较复杂的东西,通过在不同的子元素/区域判断 drag over,并且阻止事件冒泡,可以区分和标记不同的插入位置。

然后就是尝试优化创建 shape 的流程。目前更改后的流程依然是:创建 shape 会先调用 selectToolAtom,生成一些数值,但是依然保存在 tool 这边。在一些操作完成之后,把数据推到 appendShapeAtom 创建 shape。目前修改为 appendShapeAtom 这里生成 name,weight 以及 groupId。然后原本通过 effect 来更新 shapes 的逻辑也需要调整,不然会让旧的生成的 name 覆盖现有的,目前修改为仅仅复制坐标相关的数据过来。

感觉这个流程还是需要整理和优化。

2025-06-01

更新 #DEV/svg-tools

创建了新的 Stroke/Fill/Stroke Style(Line Style) 分离的 StyleForm,暂时没有必要搞成 tab,不过,对 stroke 和 fill 做了一点修改,借鉴 Inkscape 的设计,把 none / currentColor / undefined 这种特殊值挪到顶上,和颜色并列。

改掉了 shape tool 创建 shape 的流程,现在不需要一堆 load atom 和 effect 了,创建 shape 只要调用 appendShapeAtom,然后通过 agent atom 就可以直接读写当前的 shape。

2025-06-02

晚上回家继续写代码,Headless UI 没办法创建嵌套的 DropDown Menu,而且也没有 ContextMenu 的支持。搜索的时候,发现 RadixUI 有完善的支持,而且也是不自带样式的,于是立刻采用。并且还有一点很好的是,不用像 Shadcn UI 那样面对一大堆陌生的 className,以及意料之外的问题。

今天重写了 PathCommand 创建和更新的逻辑,使用 activePathCommandAtom 去获取和更新当前的 command,逻辑清晰了很多。

增加了 H 和 V 命令的支持。

DEV/svg-tools

2025-06-04

更新 DEV/svg-tools ,在逐步用 Shadcn UI 替换掉 DaisyUI。

2025-06-05

更新了 svg-tools,重新实现了一个 TreeView,用 Context 在子组件之间传递状态数据。在这个基础上重新创建了 ShapeTree 相关的组件,实现了样式和逻辑的分离。

Inkscape 做了两种选择工具,一个选择工具用来移动和缩放整个图形,一个节点工具用来选中路径,操作点。感觉可以借鉴这部分的设计。

2025-06-06

回来差不多不到九点,写代码,更新 svg-tools,清理了一些陈旧代码,以及重新实现了重命名的功能。在 shape 上面右键的菜单里增加了重命名,不过,如果是 autoFocus,输入框总是在 focus 之后立刻 blur,导致会退出编辑状态。

2025-08-02

迁移旧的 svg-tools 成 svg-maker,新的项目不依赖 remix 的 server,直接使用 react-router 前端路由。

© 2026 香蕉引擎故障报告

🌱 Powered by Hugo with theme Dream.

关于

要怎么介绍自己呢,🤔。

很早以前是作为 Web 前端在学习的,但是工作第一年就成为了全干工程师。喜欢尝试各种东西,什么都会一点。

一直很喜欢 Ebiten 游戏引擎 ,特别简洁,用它做过一些小东西,可以查看这个分类 。另外特别推荐这个木鱼 ,是一个相对完整的小玩意儿,包含手搓的一个简单的 UI 框架;支持鼠标和键盘操作;有多语言和主题切换功能;同时支持 Web 端和客户端。它的源代码在 bin16/wooden-fish

主题

网站基于 Hugo,当前使用的是 hugo-theme-dream 主题的修改版 ,根据我的需要,做了一些对 PaperMod 的兼容。

我自己也写过主题 ,但是没有别人写的好看。

正在从我的笔记中往外搬运内容

等待更新:

  • 从《锈湖》中学了些什么东西
  • 我拿 React 写解谜游戏的经过
  • 基于 Pocketbase 的 Pocket Memos
  • 数独!