使用fabric實現(xiàn)恢復和撤銷功能的實例詳解
介紹
在圖形編輯器中,撤銷和恢復是一個非常常見的功能了,但是搜了下,網上好像也沒有太多相關的文章 可能是因為canvas相關的資料確實太少了吧
其實實現(xiàn)撤銷和恢復并不難,因為fabric是支持把當前畫布中的內容導出為json的,并也支持導入json到畫布中去
當我們有了這兩個基本的能力,剩下的本質上就是如何監(jiān)聽畫布狀態(tài)的變更和操作狀態(tài)如何存取的問題了
我這里用了比較簡單和直接的辦法,定義了 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
何時更新存儲的狀態(tài)?
需要監(jiān)聽的方法
回到上面的問題,我們需要怎么監(jiān)聽畫布狀態(tài)的變更? 這個問題實際上很簡單,我們可以通過監(jiān)聽 fabric 的回調事件來進行處理 正常情況我認為監(jiān)聽
'object:added' 'object:removed' 'object:modified' 'object:skewing'
四個事件已經足夠收集到畫布的變更了,甚至 object:skewing 其實都不太有必要 并且考慮到有些情況下可能需要取消監(jiān)聽,所以我這里定義了兩個方法 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
如何保存畫布變更的狀態(tài)
將當前畫布轉換為 json
// 獲取當前畫布的 JSON 描述 const canvasState = this.canvas.toDatalessJSON() const currentStateString = JSON.stringify(canvasState)
我這里用的是 toDatalessJSON() 方法,而不是 toJSON(),主要是因為以下的
- toDatalessJSON主要用于在需要減小序列化后數(shù)據(jù)大小的情況下,特別是在處理復雜的SVG圖形時。由于SVG圖形載入后通常是以ObjectPaths來保存的,因此大的SVG圖形會有很多的Path數(shù)據(jù),直接序列化會導致JSON數(shù)據(jù)過長。
- toDatalessJSON方法可以將這些Path數(shù)據(jù)用路徑來代替,以減小序列化后的數(shù)據(jù)量。但需要注意的是,這需要手動設置sourcePath以便在下次使用時能夠找到對應的資源。
toDatalessJSON 和 toJSON 的主要區(qū)別
- toJSON方法會完整地將畫布上的所有對象及其屬性序列化為JSON數(shù)據(jù),包括Path等詳細數(shù)據(jù)。
- toDatalessJSON則會嘗試優(yōu)化這些數(shù)據(jù),通過用路徑代替詳細數(shù)據(jù)來減小數(shù)據(jù)量。
判斷當前狀態(tài)和撤銷堆棧中最后一個狀態(tài)是否相同 我們這里需要做一個邊界的處理,如果當前保存的狀態(tài)和最后一個撤銷狀態(tài)相同的情況下,則不需要對它進行保存,避免有些多余的保存影響到了撤銷和恢復的功能
// 判斷當前狀態(tài)和撤銷堆棧中最后一個狀態(tài)是否相同 if (this.undoStack.length > 0) { const lastUndoStateString = this.undoStack[this.undoStack.length - 1] if (currentStateString === lastUndoStateString) { // 如果當前狀態(tài)和最后一個撤銷狀態(tài)相同,則不保存 console.log('Current canvas state is identical to the last saved state. Skipping save.') return } }
將畫布狀態(tài)保存到撤銷堆棧
// 將畫布狀態(tài)保存到撤銷堆棧 this.undoStack.push(currentStateString)
限制撤銷堆棧的大小以節(jié)省內存
我們這里限制一下保存的狀態(tài),避免在堆棧中保存了太多的狀態(tài)占用了太多內存,我這里就暫且只保存30步,當超出的情況下則把前面的給頂出去
private readonly maxUndoStackSize: number = 30 // 最大撤銷堆棧大小 ... // 限制撤銷堆棧的大小以節(jié)省內存 if (this.undoStack.length > this.maxUndoStackSize) { this.undoStack.shift() // 移除最舊的狀態(tài) }
如何自定義保存的狀態(tài)或時機
有很多時候我們其實并不想每一步操作都進行保存,例如我們在進行批量創(chuàng)建操作時,由于我們實際上的操作是一個個插入的,如果我們只是單純地把每一步狀態(tài)都記錄了,那么我們在撤銷的時候也只會一個個撤回去,跟我們原本的一次性創(chuàng)建N個元素的操作并不是逆向操作 這時候我們就需要去自定義一些保存的時機了,我這里暫且定義了兩種方式:
- 忽略下一次畫布變更的保存
- 自定義停止在當前流程中的狀態(tài)保存,以及自定義開始保存
忽略下一次畫布變更的保存
這個其實很簡單,我們直接定義一個狀態(tài)位來記錄一下即可
// 用于忽略下一次操作的保存 private ignoreNextSave: boolean = false ignoreNextStateSave = () => { this.ignoreNextSave = true }
在保存的時候將狀態(tài)位進行重置
private saveStateIfNotRestoring = () => { if (!this.ignoreNextSave && this.hasListener) { this.saveCustomState() } this.ignoreNextSave = false // 重置標志 }
自定義停止在當前流程中的狀態(tài)保存,以及自定義開始保存
這里跟上面其實差不多,也是定義了一個狀態(tài)位來保存當前是否屬于允許保存的情況
private hasListener: boolean = true changeHistoryListenerStatus = (hasListener: boolean) => { this.hasListener = hasListener }
不過這里的狀態(tài)位就是由用戶自己控制了
自定義撤銷功能
在這里我們需要去處理的是,在恢復的過程中我們其實會存在多次觸發(fā)fabric回調的情況,所以我們在恢復的情況下需要暫時停止監(jiān)聽,等到操作完成后再注冊監(jiān)聽的事件
customUndo = () => { if (this.undoStack.length > 1) { // 取消事件監(jiān)聽器 this.offHistoryListener() // 將當前狀態(tài)彈出并保存到恢復堆棧 this.redoStack.push(this.undoStack.pop()!) // 獲取撤銷后的狀態(tài) const previousState = this.undoStack[this.undoStack.length - 1] this.canvas.clear() // 臨時禁用事件監(jiān)聽, 但是點擊一次存在多次監(jiān)聽更新的情況下不管用,所以可以考慮手動去掉事件監(jiān)聽器 this.isRestoring = true this.canvas.loadFromJSON(previousState, () => { // 重新注冊事件監(jiān)聽器 this.initHistoryListener() this.canvas.renderAll() this.isRestoring = false }) } }
自定義恢復功能
這里也和上面一樣
customRedo = () => { if (this.redoStack.length > 0) { // 取消事件監(jiān)聽器 this.offHistoryListener() // 將最后的恢復狀態(tài)彈出并保存到撤銷堆棧 this.undoStack.push(this.redoStack.pop()!) // 獲取恢復的狀態(tài) const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1]) // 臨時禁用事件監(jiān)聽 this.isRestoring = true this.canvas.clear() this.canvas.loadFromJSON(nextState, () => { // 重新注冊事件監(jiān)聽器 this.initHistoryListener() this.canvas.renderAll() this.isRestoring = false }) } }
整體實現(xiàn)
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 // 初始狀態(tài) 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) // 判斷當前狀態(tài)和撤銷堆棧中最后一個狀態(tài)是否相同 if (this.undoStack.length > 0) { const lastUndoStateString = this.undoStack[this.undoStack.length - 1] if (currentStateString === lastUndoStateString) { // 如果當前狀態(tài)和最后一個撤銷狀態(tài)相同,則不保存 console.log('Current canvas state is identical to the last saved state. Skipping save.') return } } // 將畫布狀態(tài)保存到撤銷堆棧 this.undoStack.push(currentStateString) // 輸出保存信息 console.log('saveCustomState', this.undoStack, this.redoStack) // 限制撤銷堆棧的大小以節(jié)省內存 if (this.undoStack.length > this.maxUndoStackSize) { this.undoStack.shift() // 移除最舊的狀態(tài) } } customUndo = () => { if (this.undoStack.length > 1) { // 取消事件監(jiān)聽器 this.offHistoryListener() // 將當前狀態(tài)彈出并保存到恢復堆棧 this.redoStack.push(this.undoStack.pop()!) // 獲取撤銷后的狀態(tài) const previousState = this.undoStack[this.undoStack.length - 1] this.canvas.clear() // 臨時禁用事件監(jiān)聽, 但是點擊一次存在多次監(jiān)聽更新的情況下不管用,所以可以考慮手動去掉事件監(jiān)聽器 this.isRestoring = true this.canvas.loadFromJSON(previousState, () => { // 重新注冊事件監(jiān)聽器 this.initHistoryListener() this.canvas.renderAll() this.isRestoring = false }) } } customRedo = () => { if (this.redoStack.length > 0) { // 取消事件監(jiān)聽器 this.offHistoryListener() // 將最后的恢復狀態(tài)彈出并保存到撤銷堆棧 this.undoStack.push(this.redoStack.pop()!) // 獲取恢復的狀態(tài) const nextState = JSON.parse(this.undoStack[this.undoStack.length - 1]) // 臨時禁用事件監(jiān)聽 this.isRestoring = true this.canvas.clear() this.canvas.loadFromJSON(nextState, () => { // 重新注冊事件監(jiān)聽器 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實現(xiàn)恢復和撤銷功能的實例詳解的詳細內容,更多關于fabric實現(xiàn)恢復和撤銷的資料請關注腳本之家其它相關文章!
相關文章
javascript 動態(tài)生成css代碼的兩種方法
這篇文章主要介紹了javascript 動態(tài)生成css代碼的兩種方法,有時候我們需要利用js來動態(tài)生成頁面上style標簽中的css代碼,下面就給大家介紹兩種方法,需要的朋友可以參考下2017-03-03javascript實現(xiàn)滾動效果的數(shù)字時鐘實例
這篇文章主要是介紹使用javascript來實現(xiàn)數(shù)字時鐘滾動的效果,非常實用,有需要的朋友們可以來參考學習。2016-07-07layui 表格操作列按鈕動態(tài)顯示的實現(xiàn)方法
今天小編就為大家分享一篇layui 表格操作列按鈕動態(tài)顯示的實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09layui之table checkbox初始化時選中對應選項的方法
今天小編就為大家分享一篇layui之table checkbox初始化時選中對應選項的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09前端實現(xiàn)文本超出指定行數(shù)顯示"展開"和"收起"效果詳細步驟
本文介紹如何使用JavaScript原生代碼實現(xiàn)文本折疊展開效果,并提供方法指導如何在Vue或React等框架中修改實現(xiàn),詳細介紹了創(chuàng)建整體框架、設置樣式及利用JS控制元素的步驟,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-10-10