高并发网站建设,高端商务经纪网站建设,网站开发什么语言比较快,网站设计的资质叫什么大家好#xff0c;我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录#xff0c;做到撤销重做。
其实就是版本号的更替。每个版本保存一个状态。
数据结构
要记录图形编辑器的历史记录#xff0c;支持撤销重做功能#xff0c;需要两个栈#xff1a;撤销#xff0…大家好我是前端西瓜哥。今天讲一下图形编辑器如何实现历史记录做到撤销重做。
其实就是版本号的更替。每个版本保存一个状态。
数据结构
要记录图形编辑器的历史记录支持撤销重做功能需要两个栈撤销undo栈和重做redo栈。
每当用户进行一个操作比如移动一个图形就会产生一个新的版本将这个操作产生的状态保持加入到 undo 栈顶此外 redo 栈会清空。因为用户可能撤销了几次然后产生了新的操作无法重做它们了。
当用户撤销undo 栈出栈并放到 redo 栈然后使用 undo 栈顶的状态。当用户重做时redo 栈出栈再放到 undo 栈上并应用 undo 栈顶的状态。
原理大概这样。
浏览器的回退前进的表现其实就是一个很常见的例子。
数据结构还有另一种方案双向链表加两个指针一个指针指向当前版本状态另一个指针指向 redo 最后一次可执行到达的状态。
然后是如果要支持协同的场景你的撤回不会回到之前的版本而是将之前的版本的状态拿出来作为一个新的版本。
然后是协同中你不能撤回别人的操作只能撤回自己的并且要用协同算法处理和其他协同者的冲突逻辑。
要保存哪些状态
那么我们的状态要保存哪些状态呢
图形树数据图形树需要的引用一些设置
图形树是必要的我们需要用它渲染画布内容。此外还有游离在图形树之外的被用到的对象比如图层、被多次引用的图形。你可以也把它们也放到图形树里面去。
最后是一些需要共享的设置比如表格的行高、筛选条件等。
像是颜色主题、国际化语言设置则不需要历史记录它是用户自己选择的个性化定制。
我们看具体的几种实现。
全量快照
每次操作的到的新状态完全拷贝一份保存起来。
因为对象如果只是浅拷贝其中的引用对象可能会被意外的修改通常我们会选择 序列化成字符串 保存即JSON.stringify。撤销重做的时候再解析出来作为当前状态。
优点是实现简单。
缺点是当状态很大的时候每次生成快照都会比较耗时且操作很多产生很多版本时需要大量的内存空间保证这些完整状态。
如果画布上有一万个独立的实体就意味着每进行一次操作就要将这个一万个实体深拷贝一份。100 次就是 100w很恐怖。
仅推荐简单的图形编辑器使用或者做 demo 用。
补丁patch
全量快照让编辑器的上限很低不是最优解。
一种更好的解法是 打补丁patch。
基于上一个版本 1打一个补丁变成下一个版本 2。同时我们记录一个反向的补丁撤回的时候能通过它从版本 2 回到版本 1。
这个方案对应了设计模式的 命令模式我们构建 Command 类这个类有 execute、redo、undo 方法这些方法会对传入的旧的状态对象打补丁得到一个新的状态。
比如添加矩形命令execute 和 redo 时我们会往图形树的末尾加一个矩形对象undo 就是将这个矩形从图形树中移除。undo 栈和 redo 栈此时记录的就是一个个 command 对象了。 纯纯用朴实无华的命令模式去实现还是有点坑的。因为要实现的命令太多了比如添加图形、修改图形属性、删除图形、对几个图形做右对齐等这些都要自己一个个实现 redo 和 undo。复杂一点就要抓瞎建议找一些轮子。比如 immer、y.js。
使用补丁方案还有一个好处就方便实现 “动作” 功能。当然这不是一个优先级很高的功能
比如我们想要给一个图形先顺时针旋转 45 度然后向右移动 10 个单位我们希望记录这两个操作给其他图形也应用这些操作。
快照的方式就不好搞或许我们可以对比新旧状态找不同推断出行为但不好搞。因为属性的变化可能来自不同的操作比如移动可以通过移动工具相对位移产生也可能直接属性面板改 x 值也可能是通过对齐操作产生的。
patch 就很适合。
什么时候保存状态
我们需要确认一个操作完成的时刻将它加入到历史记录中。
我们操作图形会产生一些 中间状态。比如移动一个图形拖拽的过程中不生产一个历史版本直到拖拽结束才记录。
一种方式是操作图形的替身操作结束后才更新真正的状态。
一些编辑器比如 Adobe Illustrator、AutoCAD我们在操作图形的时候会看到一个临时的替身就是将被选中图形的轮廓线或拷贝做鼠标的跟随释放后才真正修改图形属性。
还比如颜色的修改在拾色器中挑选颜色时不会立即修改图形在点击确认才真正修改图形。 另一种方式是直接操作真正的状态在操作结束的时候记录这个时刻的状态。 第一种方式的好处是状态没有中间状态替身操作完计算出新状态应用到真正的状态上就好了。
第二种方式就要额外在操作开始时保存原始状态的快照因为之后我们会产生中间状态然后在操作结束后计算 patch。
但第二种方式用户体验会更好些用户能实时看到一个图形的变化判断是不是自己需要的效果而不是看到一个 “通往未来的幻影”。
结尾
我是前端西瓜哥关注我学习前端不迷路。