IT俱乐部 JavaScript 使用fabric实现恢复和撤销功能的实例详解

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

介绍

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的回调事件来进行处理 正常情况我认为监听

1
2
3
4
'object:added'
'object:removed'
'object:modified'
'object:skewing'

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

1
2
3
// 获取当前画布的 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则会尝试优化这些数据,通过用路径代替详细数据来减小数据量。

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

1
2
3
4
5
6
7
8
9
// 判断当前状态和撤销堆栈中最后一个状态是否相同
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
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
private saveStateIfNotRestoring = () => {
  if (!this.ignoreNextSave && this.hasListener) {
    this.saveCustomState()
  }
  this.ignoreNextSave = false // 重置标志
}

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

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

1
2
3
4
5
private hasListener: boolean = true
 
changeHistoryListenerStatus = (hasListener: boolean) => {
  this.hasListener = hasListener
}

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

自定义撤销功能

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
    })
  }
}

自定义恢复功能

这里也和上面一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
    })
  }
}

整体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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实现恢复和撤销的资料请关注IT俱乐部其它相关文章!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/navsub/js/12282.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部