欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用fabric實(shí)現(xiàn)恢復(fù)和撤銷功能的實(shí)例詳解

 更新時(shí)間:2024年06月30日 09:39:10   作者:日明  
在圖形編輯器中,撤銷和恢復(fù)是一個(gè)非常常見(jiàn)的功能了,但是搜了下,網(wǎng)上好像也沒(méi)有太多相關(guān)的文章 可能是因?yàn)閏anvas相關(guān)的資料確實(shí)太少了吧,所以本文給大家介紹了如何基于 fabric 實(shí)現(xiàn)恢復(fù)、撤銷功能,需要的朋友可以參考下

介紹

在圖形編輯器中,撤銷和恢復(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)文章

  • JS實(shí)現(xiàn)留言板功能[樓層效果展示]

    JS實(shí)現(xiàn)留言板功能[樓層效果展示]

    小編最近在基于js實(shí)現(xiàn)留言板功能,實(shí)現(xiàn)的功能有發(fā)布人和發(fā)布內(nèi)容做非空校驗(yàn),樓層效果展示和發(fā)布時(shí)間展示。具體實(shí)例代碼大家參考下本文
    2017-12-12
  • JS 自執(zhí)行函數(shù)原理及用法

    JS 自執(zhí)行函數(shù)原理及用法

    這篇文章主要介紹了JS 自執(zhí)行函數(shù)原理及技巧,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-08-08
  • javascript 動(dòng)態(tài)生成css代碼的兩種方法

    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-03
  • Javascript中For In語(yǔ)句用法實(shí)例

    Javascript中For In語(yǔ)句用法實(shí)例

    這篇文章主要介紹了Javascript中For In語(yǔ)句用法,實(shí)例分析了javascript使用For In語(yǔ)句遍歷數(shù)組的技巧,需要的朋友可以參考下
    2015-05-05
  • javascript實(shí)現(xiàn)滾動(dòng)效果的數(shù)字時(shí)鐘實(shí)例

    javascript實(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-07
  • layui 表格操作列按鈕動(dòng)態(tài)顯示的實(shí)現(xiàn)方法

    layui 表格操作列按鈕動(dòng)態(tài)顯示的實(shí)現(xiàn)方法

    今天小編就為大家分享一篇layui 表格操作列按鈕動(dòng)態(tài)顯示的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2019-09-09
  • layui之table checkbox初始化時(shí)選中對(duì)應(yīng)選項(xiàng)的方法

    layui之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
  • javascript表格隨機(jī)排序代碼

    javascript表格隨機(jī)排序代碼

    非常不錯(cuò)的思路,用js實(shí)現(xiàn)表格的隨機(jī)排序,建議大家看代碼,學(xué)習(xí)編程思路
    2008-09-09
  • 前端實(shí)現(xiàn)文本超出指定行數(shù)顯示"展開(kāi)"和"收起"效果詳細(xì)步驟

    前端實(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-10
  • TypeScript實(shí)現(xiàn)單鏈表的示例代碼

    TypeScript實(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

最新評(píng)論