脚本之家

电脑版
提示:原网页已由神马搜索转码, 内容由www.jb51.net提供.
您的位置:首页网络编程JavaScriptjavascript技巧→ fabric实现恢复和撤销

使用fabric实现恢复和撤销功能的实例详解

  更新时间:2024年06月30日 09:39:10  作者:日明 
在图形编辑器中,撤销和恢复是一个非常常见的功能了,但是搜了下,网上好像也没有太多相关的文章 可能是因为canvas相关的资料确实太少了吧,所以本文给大家介绍了如何基于 fabric 实现恢复、撤销功能,需要的朋友可以参考下

介绍

在图形编辑器中,撤销和恢复是一个非常常见的功能了,但是搜了下,网上好像也没有太多相关的文章 可能是因为canvas相关的资料确实太少了吧

其实实现撤销和恢复并不难,因为fabric是支持把当前画布中的内容导出为json的,并也支持导入json到画布中去

当我们有了这两个基本的能力,剩下的本质上就是如何监听画布状态的变更和操作状态如何存取的问题了

我这里用了比较简单和直接的办法,定义了 undoStack 和 redoStack 两个 stack 来进行记录和存取

class CanvasStateManager {
protected canvas: canvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []

constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
}
}
export default CanvasStateManager

何时更新存储的状态?

需要监听的方法

回到上面的问题,我们需要怎么监听画布状态的变更? 这个问题实际上很简单,我们可以通过监听 fabric 的回调事件来进行处理 正常情况我认为监听

'object:added'
'object:removed'
'object:modified'
'object:skewing'

四个事件已经足够收集到画布的变更了,甚至 object:skewing 其实都不太有必要 并且考虑到有些情况下可能需要取消监听,所以我这里定义了两个方法 initHistoryListener 和 offHistoryListener

class CanvasStateManager {
protected canvas: canvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []

constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
this.initHistoryListener()
}
initHistoryListener = async () => {
this.canvas.on({
[ICanvasEvent.OBJECT_ADDED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_MODIFIED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_REMOVED]: this.saveStateIfNotRestoring
})
}
offHistoryListener = () => {
this.canvas.off(ICanvasEvent.OBJECT_ADDED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_MODIFIED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_REMOVED, this.saveStateIfNotRestoring)
}
}
export default CanvasStateManager

如何保存画布变更的状态

将当前画布转换为 json

 // 获取当前画布的 JSON 描述
const canvasState = this.canvas.toDatalessJSON()
const currentStateString = JSON.stringify(canvasState)

我这里用的是 toDatalessJSON() 方法,而不是 toJSON(),主要是因为以下的

  • toDatalessJSON主要用于在需要减小序列化后数据大小的情况下,特别是在处理复杂的SVG图形时。由于SVG图形载入后通常是以ObjectPaths来保存的,因此大的SVG图形会有很多的Path数据,直接序列化会导致JSON数据过长。
  • toDatalessJSON方法可以将这些Path数据用路径来代替,以减小序列化后的数据量。但需要注意的是,这需要手动设置sourcePath以便在下次使用时能够找到对应的资源。

toDatalessJSON 和 toJSON 的主要区别

  • toJSON方法会完整地将画布上的所有对象及其属性序列化为JSON数据,包括Path等详细数据。
  • toDatalessJSON则会尝试优化这些数据,通过用路径代替详细数据来减小数据量。

判断当前状态和撤销堆栈中最后一个状态是否相同 我们这里需要做一个边界的处理,如果当前保存的状态和最后一个撤销状态相同的情况下,则不需要对它进行保存,避免有些多余的保存影响到了撤销和恢复的功能

  // 判断当前状态和撤销堆栈中最后一个状态是否相同
if (this.undoStack.length > 0) {
const lastUndoStateString = this.undoStack[this.undoStack.length - 1]
if (currentStateString === lastUndoStateString) {
// 如果当前状态和最后一个撤销状态相同,则不保存
console.log('Current canvas state is identical to the last saved state. Skipping save.')
return
}
}

将画布状态保存到撤销堆栈

// 将画布状态保存到撤销堆栈
this.undoStack.push(currentStateString)

限制撤销堆栈的大小以节省内存

我们这里限制一下保存的状态,避免在堆栈中保存了太多的状态占用了太多内存,我这里就暂且只保存30步,当超出的情况下则把前面的给顶出去

private readonly maxUndoStackSize: number = 30 // 最大撤销堆栈大小
...
// 限制撤销堆栈的大小以节省内存
if (this.undoStack.length > this.maxUndoStackSize) {
this.undoStack.shift() // 移除最旧的状态
}

如何自定义保存的状态或时机

有很多时候我们其实并不想每一步操作都进行保存,例如我们在进行批量创建操作时,由于我们实际上的操作是一个个插入的,如果我们只是单纯地把每一步状态都记录了,那么我们在撤销的时候也只会一个个撤回去,跟我们原本的一次性创建N个元素的操作并不是逆向操作 这时候我们就需要去自定义一些保存的时机了,我这里暂且定义了两种方式:

  • 忽略下一次画布变更的保存
  • 自定义停止在当前流程中的状态保存,以及自定义开始保存

忽略下一次画布变更的保存

这个其实很简单,我们直接定义一个状态位来记录一下即可

// 用于忽略下一次操作的保存
private ignoreNextSave: boolean = false
ignoreNextStateSave = () => {
this.ignoreNextSave = true
}

在保存的时候将状态位进行重置

  private saveStateIfNotRestoring = () => {
if (!this.ignoreNextSave && this.hasListener) {
this.saveCustomState()
}
this.ignoreNextSave = false // 重置标志
}

自定义停止在当前流程中的状态保存,以及自定义开始保存

这里跟上面其实差不多,也是定义了一个状态位来保存当前是否属于允许保存的情况

  private hasListener: boolean = true
changeHistoryListenerStatus = (hasListener: boolean) => {
this.hasListener = hasListener
}

不过这里的状态位就是由用户自己控制了

自定义撤销功能

在这里我们需要去处理的是,在恢复的过程中我们其实会存在多次触发fabric回调的情况,所以我们在恢复的情况下需要暂时停止监听,等到操作完成后再注册监听的事件

  customUndo = () => {
if (this.undoStack.length > 1) {
// 取消事件监听器
this.offHistoryListener()
// 将当前状态弹出并保存到恢复堆栈
this.redoStack.push(this.undoStack.pop()!)
// 获取撤销后的状态
const previousState = this.undoStack[this.undoStack.length - 1]
this.canvas.clear()
// 临时禁用事件监听, 但是点击一次存在多次监听更新的情况下不管用,所以可以考虑手动去掉事件监听器
this.isRestoring = true
this.canvas.loadFromJSON(previousState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}

自定义恢复功能

这里也和上面一样

  customRedo = () => {
if (this.redoStack.length > 0) {
// 取消事件监听器
this.offHistoryListener()
// 将最后的恢复状态弹出并保存到撤销堆栈
this.undoStack.push(this.redoStack.pop()!)
// 获取恢复的状态
const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1])
// 临时禁用事件监听
this.isRestoring = true
this.canvas.clear()
this.canvas.loadFromJSON(nextState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}

整体实现

class CanvasStateManager {
protected canvas: Owl.ICanvas
protected editor: IEditor
private undoStack: string[] = []
private redoStack: string[] = []
private isRestoring: boolean = false
// 用于忽略下一次操作的保存
private ignoreNextSave: boolean = false
private hasListener: boolean = true
private readonly maxUndoStackSize: number = 30 // 最大撤销堆栈大小
static apis = [
'clearCustomHistory',
'saveCustomState',
'customUndo',
'customRedo',
'ignoreNextStateSave',
'initHistoryListener',
'offHistoryListener',
'changeHistoryListenerStatus'
]
constructor(canvas: Owl.ICanvas, editor: Owl.IEditor) {
this.canvas = canvas
this.editor = editor
// 初始状态
this.saveCustomState()
this.initHistoryListener()
}
private saveStateIfNotRestoring = () => {
if (!this.isRestoring && !this.ignoreNextSave && this.hasListener) {
console.log('saveStateIfNotRestoring -> saveCustomState')
this.saveCustomState()
}
this.ignoreNextSave = false // 重置标志
}
clearCustomHistory = () => {
this.undoStack = []
this.redoStack = []
this.saveCustomState()
}
saveCustomState = () => {
// 获取当前画布的 JSON 描述
const canvasState = this.canvas.toDatalessJSON()
const currentStateString = JSON.stringify(canvasState)
// 判断当前状态和撤销堆栈中最后一个状态是否相同
if (this.undoStack.length > 0) {
const lastUndoStateString = this.undoStack[this.undoStack.length - 1]
if (currentStateString === lastUndoStateString) {
// 如果当前状态和最后一个撤销状态相同,则不保存
console.log('Current canvas state is identical to the last saved state. Skipping save.')
return
}
}
// 将画布状态保存到撤销堆栈
this.undoStack.push(currentStateString)
// 输出保存信息
console.log('saveCustomState', this.undoStack, this.redoStack)
// 限制撤销堆栈的大小以节省内存
if (this.undoStack.length > this.maxUndoStackSize) {
this.undoStack.shift() // 移除最旧的状态
}
}
customUndo = () => {
if (this.undoStack.length > 1) {
// 取消事件监听器
this.offHistoryListener()
// 将当前状态弹出并保存到恢复堆栈
this.redoStack.push(this.undoStack.pop()!)
// 获取撤销后的状态
const previousState = this.undoStack[this.undoStack.length - 1]
this.canvas.clear()
// 临时禁用事件监听, 但是点击一次存在多次监听更新的情况下不管用,所以可以考虑手动去掉事件监听器
this.isRestoring = true
this.canvas.loadFromJSON(previousState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
customRedo = () => {
if (this.redoStack.length > 0) {
// 取消事件监听器
this.offHistoryListener()
// 将最后的恢复状态弹出并保存到撤销堆栈
this.undoStack.push(this.redoStack.pop()!)
// 获取恢复的状态
const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1])
// 临时禁用事件监听
this.isRestoring = true
this.canvas.clear()
this.canvas.loadFromJSON(nextState, () => {
// 重新注册事件监听器
this.initHistoryListener()
this.canvas.renderAll()
this.isRestoring = false
})
}
}
ignoreNextStateSave = () => {
this.ignoreNextSave = true
}
changeHistoryListenerStatus = (hasListener: boolean) => {
this.hasListener = hasListener
}
initHistoryListener = async () => {
this.canvas.on({
[ICanvasEvent.OBJECT_ADDED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_MODIFIED]: this.saveStateIfNotRestoring,
[ICanvasEvent.OBJECT_REMOVED]: this.saveStateIfNotRestoring
})
}
offHistoryListener = () => {
this.canvas.off(ICanvasEvent.OBJECT_ADDED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_MODIFIED, this.saveStateIfNotRestoring)
this.canvas.off(ICanvasEvent.OBJECT_REMOVED, this.saveStateIfNotRestoring)
}
}
export default CanvasStateManager

以上就是使用fabric实现恢复和撤销功能的实例详解的详细内容,更多关于fabric实现恢复和撤销的资料请关注脚本之家其它相关文章!

相关文章

    • 这篇文章主要介绍了JavaScript事件发布/订阅模式,结合实例形式简单分析了javascript发布/订阅模式的概念、原理及简单使用方法,需要的朋友可以参考下
      2018-08-08
    • 通过js实现分页的代码,一般情况下需要指定页数,脚本之家以前也发布过一些,大家结合下即可。
      2010-09-09
    • 这篇文章主要为大家介绍了小程序canvas手写签名适配PC实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
      2023-04-04
    • 这篇文章主要介绍了JavaScript中的异步能省掉await吗?一直以来,困扰我的一个问题是JavaScript中,能否实现不带await的异步。今天我终于把这个问题想通了然后分享给大家,希望对大家的学习过程有所帮助
      2021-12-12
    • 这篇文章主要为大家详细介绍了swiper Scrollbar滚动条组件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
      2019-09-09
    • 变量是存储信息的容器(JS的变量名是区分大小写的),下面这篇文章主要给大家介绍了关于JS进阶指南之变量和类型的相关资料,文章通过实例代码介绍的非常详细,需要的朋友可以参考下
      2022-05-05
    • 这篇文章主要介绍了JS基于ocanvas插件实现的简单画板效果,结合实例形式分析了ocanvas插件实现画板的相关技巧,并附代码demo源码供读者下载参考,需要的朋友可以参考下
      2016-04-04
    • 文字如何实现打字的效果呢?在浏览网页的时候也经常能看到这种效果。本文给大家汇总介绍了几种打字效果的文字特效,文字一个一个地打印在页面上。
      2015-08-08
    • 很棒的JavaScript选项卡切换效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
      2016-07-07
    • pixijs 是一个强大的 Web Canvas 2D 库,以其强大性能而著称。这篇文章主要带大家学习一下PixiJS是如何实现常见图形绘制的,希望对大家有所帮助
      2023-02-02

    最新评论