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