canvas模塊 version 1.0.1 思路速記

概要

項目UI改版,對於炫酷展現內容做出了必定的要求,因此本身嘗試的編寫了一次canvas的繪製模塊的內容,在這裏作個總結和回顧。固然本次繪製模塊徹底是使用canvas的一個實現,並無涉及到svg或者css。只是我這個菜鳥的嘗試,大神請忽視之。css

實現

需求內容

canvas模塊的需求主要是以下:前端

1.複雜圖形的繪製:canvas爲咱們提供了不少的基礎模型的內容,可是有的時候UI大大的設計稿簡直是要把人往絕路上面逼(類目ING)。簡單一點的例如,有弧度的矩形,彎曲的三角形等等,咱們經常須要對這一類的內容進行腦洞大開式的開發。因此咱們須要有對複雜會圖形的繪製能力,同時也是但願能夠對這些複雜的圖形進行復用。vue

2.圖形動畫展現:動畫的展現對於前段來講必不可少的,炫酷的前端不可能老是一成不變的靜態內容吧。可是動畫的主要難題在於,可能對於同一幀的屏幕變更之中,不一樣的組件的變化是不相同的,如何同調,如何組合這些內容的變化。es6

3.圖形組件之間的數據交互:同一個展現內容,須要不一樣的繪製模塊來相互合做,有的時候不一樣的組件之間須要有必定的數據交互,例如某位移動畫,其餘組件的移動位置與速度,須要當前選中的組件的速度與位置偏移來進行計算。web

4.圖形組件的事件:最後,也是最終要的一點,圖形是由許多的組件內容來進行結合從而展現的,可是用戶在操做的時候,不一樣的方式對於不一樣的組件來講可能須要有不同的交互。而且此時也十分考驗組建的交互。canvas

基礎設計

1.組件化繪製:

考慮到上面這些需求,咱們將單個的操做點做爲一個組建的話,例如繪製的長方形,圓形,甚至是線條。那其實複雜的內容,實際上都是由許許多多的組件進行組合而來的,因此咱們能夠考慮將圖形設計成爲組件的形式。而且複雜組件繪製的時候,能夠將其拆分紅爲自定義繪製部分以及可複用組件的部分。父組件同時須要對子組件進行內容的管理,如此也方便了父子組件之間的信息傳遞。數組

組件的形式解剖繪製的話,單個組件的內容應該是怎麼樣的呢?應該如何的去進行內容的處理呢?還有很重要的一點就是,父組件繪製的時候,咱們應該如何去繪製呢,由於canvas的內容展現其實是圖層的形式一層一層疊加的,當繪製的順序不對的話,最後的呈現效果也將會徹底不一樣。緩存

2.動畫配置

針對某一組件的動畫,canvas之中使用的方式仍是清除 - 重繪。之中咱們能夠經過使用imageData針對一些能夠不變的內容進行必定的存儲。來加速當前的繪製。問題在於何時對內容進行緩存,如何獲取相應的緩存內容。bash

3.數據交互

組件之間的數據交互,能夠經過數值傳遞的方式來進行的,prop的存在很好的幫助子組件的定製化,和可重用。那麼應該如何進行數據的傳遞,父組件之中的參數,有些須要計算以後才能傳遞給子組件,這一步應該如何,或者說怎麼去實現。svg

4.圖形事件

事件是交互的基礎,如一個繪製組件,咱們不可能將全部的交互同時綁定在這個組件之上,針對於同一個組件的不一樣的子組件,可能會針對同一類型的交互產生不一樣效果,同時這些效果之間可能也有相關性。如何達到關聯性,這點很重要。

實現內容

1.生命週期

些這個模塊的時候實際上我是借用了一部分vue的思想。首先咱們肯定的是,組建之中擁有的特定屬性。

  1. canvas:表示的是當前的繪製函數進行繪製的畫板內容。
  2. state:當前組建所擁有的的屬性內容。
  3. events:相關的事件信息和內容,記錄爲當前組件綁定的事件。
  4. animations:組件動畫事件記錄。
  5. here:斷定某一座標是否在當前的組件之中。
  6. clear:當前組件的清除方法,清除方法是咱們的達到動效繪製展現的基礎。

接下來須要肯定的其實是一個組件的運行流程,就是咱們所說的生命週期。生命週期代表了一個組件總體的運行軌跡,肯定好生命週期能夠更好的明確組建當前的狀態,以及組件能處理的事情,還有處理方式。例如當組件在初始化的環節的時候,咱們能夠肯定當前組建的狀態是initing狀態,而且他須要處理的工做和反饋實際上就是加載好當前給定的配置內容。

首先咱們能夠肯定環節性質的內容:

初始化環節:

  1. 初始化函數是必不可少的,初始化的時候主要處理的是開啓當前組件的生命週期,而後處理一部分當前給定參數內容。由於參數的變更咱們須要有捕捉,並針對當前的參數修改進行組件重繪,因此這咱們須要借鑑一下vue的數據綁架原理。在初始化以後咱們能夠對當前的數據進行一步份自定義的操做了,因此咱們能夠爲流程之中添加一個afterInit的鉤子。

  2. 在初始化以後,考慮到須要有相關的視圖,先可視,再有動效和事件交互。全部的一切都是以可視爲前提,因此這裏咱們在初始化以後的一步肯定爲繪製。因爲繪製的特殊性,其其實是依賴於屬性內容的計算得出相關的繪製結果的,因此這裏並很差確立任何的鉤子函數來改變內容,繪製前鉤子,能夠直接定義afterInit內容,繪製完成以後,在調用其餘的鉤子函數也實屬沒有什麼必要,因此決定這裏放棄鉤子。

  3. 在繪製以後實際上咱們應該對於其相關的動效進行添加了,由於動效也是屬於視覺的一部分。動效的內容其實是一份針對state內容修改的邏輯,而且須要依據傳遞的時間內容,來作一個定時運行,具體的實現咱們將會在以後更完整的說明。

  4. 最後咱們須要添加的是當前組件的交互內容,事件依據類型將會有專門的數組內容存儲全部綁定到當前類型的數據,而且依據先進先運行的方式,來運行這些回調函數。

更新重繪環節:

  1. 當咱們更新了state之中數據的時候,會觸發更新機制,開始更新週期,首先是直接更改當前的state之中的值,以後會調用afterUpdate的鉤子函數,用戶能夠在這裏記錄一些相關的變化信息,可是拒絕更改當前的state內容。

  2. 調用完成鉤子函數以後,組件的狀態將會變成可更新的狀態,而後模塊將會重畫當前的模塊內容。

銷燬週期:

  1. 模塊的內容能夠用戶手動的進行銷燬,調用destroy函數以後,組件進入銷燬週期。調用beforeDestory鉤子函數內容。在這個鉤子函數之中能夠決定某些保留信息。從而對當前的信息內容進行一處理。
  2. 銷燬過程將會解綁組件內容,刪除相關的狀態信息和用戶自定義的內容函數。
  3. 組件生命週期結束。

生命週期流程圖

2. 組件數據參數處理模塊

因爲須要針對動畫還有事件內容作特殊的處理,同時爲了擁有相對的向下文環境,因此咱們這裏採用的是將幾個不一樣的模塊內容拆分到不一樣的文件之中去相應實現。經過es6的模塊來引入須要時使用的內容。這樣咱們將會有單獨的環境,防止其之間的互不侵染。


2.1. 生命週期的初始化:

生命週期咱們上面說明過了,可是在初始化一個組件的時候咱們須要對其生命週期進行一些初始化操做,包括一私有數據以及相關的調用方法。能夠看下面這一段代碼內容:

Layer.prototype._startLifecycle = function () {
    const layer = this
    
    // 表示當前組件內容是否有相關的更新。
    // 當state之中的內容被改動的時候咱們會改變這個值。
    // 並依據當前值內容的來肯定是否重繪
    layer.$updated = false

    // 表示當前組件是否已經被銷燬。這個參數主要是由於,
    // 當數據內容被銷燬的以後其實是還有一段時間停留在內存之中
    // 咱們只是取消了相關的指向,有待垃圾清理機制來回收。
    // destoryed表示的是已經清理完成了相關的指向。
    layer.$destroyed = false
}
複製代碼

以後的話咱們還須要對生命週期之中的一些函數內容進行封裝,好比咱們的afterUpdate實際上他不只僅是爲了方便用戶調用的hook函數,同時他也承接了以後的重繪等等工做內容的銜接。因此咱們能夠編寫爲以下的形式。

Layer.prototype.$afterUpdate = function () {
    const layer = this
    // 調用用戶自定義的afterUpdate方法
    layer.afterUpdate && layer.afterUpdate(layer.state)
    
    // 須要當前的組件的父組件及上層組件做出重繪更新操做。
    layer.$willUpdate()
    
    // 須要當前組件及其子組件做出強制的重繪更新操做。
    // 強制是應爲,當前修改的參數不必定是咱們傳遞給子組件的參數
    // 可是有的時候,改變單一參數的話實際上仍是會對全組件展現有比較大的影響。
    // 因此爲了更爲精準的展現組件內容,因此子組件也須要強制更新。
    layer.$forceUpdate()
}
複製代碼

2.2. state內容初始化

state值的相關的組件屬性內容,咱們能夠放在單獨的js之中進行初始化,包括其餘一些能夠簡單賦值的參數內容,例如clear(組件清除)函數或者here(位置判別)函數。咱們單獨的將關於當前繪製內容的參數給定到state之中,而且爲每個參數設置相關的描述,已達到參數變化監控的效果,速寫方式以下:

function initState (layer, options) {
  layer.state = layer.state ? layer.state : {}
  // add origin value
  for (let key in options) {
    layer.state['$' + key] = options[key]
  }
  // set properties
  for (let key in options) {
    (function () {
      let ok = '$' + key
      let k = key
      let o = layer.state
      
      // 描述符的建立
      Object.defineProperty(o, k, {
        enumerable: true,
        configurable: true,
        get: function getState () {
        
          // 獲取值的時候並不作特殊的操做
          return o[ok]
        },
        set: function setState (newValue) {
        
          // 當時值有變更的時候咱們須要判別,並調用更新後鉤子方法。
          // afterUpdate的內容,咱們上面已經說明了。
          if (newValue !== o[ok]) {
            o['$' + k] = newValue
            callhook(layer, 'afterUpdate')
          }
        }
      })
    })()
  }
}
複製代碼

以後咱們還能夠爲state內容的設置做出相關的便捷方法,例如多內容同時設置,多參數同時獲取等等。


2.3. 其餘相關自定義方法和參數:

咱們上面有簡單提到clear,here等函數的內容,這裏咱們在着重說明一下,這些相關的參數內容,包括:

  • clear:請求展現,用於清除當前的內容的展現效果,其實咱們主要的清除方法在canvas之中的仍是clearRect,因此要在用的時候十分注意,咱們應該只傳遞最上層組件的清除方法,由於重繪的時候,咱們能夠依據上面的afterUpdate之中的邏輯來看,是必定會重繪最上層組件內容的,這是由於,清除長方形區域,有的時候每每會對旁邊的其餘組件形成繪製影響,而且這種影響每每是不可辨別的。因此咱們須要對內容進行徹底的從新繪製。
  • here:表示當前的內容的區域判別,判斷某一位置是否是在組件之中,或者相對組建的什麼方向,這一個事件每每會用在交互之中,自定義組建的時候,此函數的返回內容能夠徹底的定製化,但會的內容將會以參數的形式給到相關的事件回調函數之中,固然用戶也可使用$here自主的調用當前的方法。
  • imageData:傳遞的參數內容是一個對象,對象之中有get和put的方法,這個方法主要是用於imageData內容的操做,幫助咱們進行內容的緩存,實際上咱們的回顧一下afterData的方法咱們能夠看到實際上對父組件的內容更新是由於子組件的變化,可是同級的其餘子其實是沒有變化的,有一些並不會影響到的組件內容,仍是能夠考慮使用imageData的方式來進行像素內容的緩存的,而且在未有改變動新的狀況下重繪的話,咱們是能夠考慮使用imageData的內容的,可是這個內容須要自定義組件的時候進行考量。
  • path*:這是一個方法內容,也是必傳參數,表示的是當前組件的繪製方法,咱們繪製的時候實際上就是對當前的方法進行運行的,在這個函數之中咱們能夠有自定義繪製的部分,或者經過函數調用其餘組件的內容。
  • $parent:這個組件其實是繪製父組件的時候,模塊自動傳遞給子組件的內容,表示的就是父組件本省,在使用組建的時候是不須要編寫進去的。
  • delay:表示當前組件將會被延遲繪製。考慮到圖層展現的問題有一些內容將會須要繪製在其餘的組件上方。因此擁有了當前參數,因此此處使用delay做爲標識。以後的話我是比較想要改爲以z-index參數內容做爲基礎的繪製形式,來肯定圖層的繪製高低。
  • z_index:圖層參數,以後的版本會被引入(version - 2)

暫時咱們肯定的就是上面這些須要組件傳遞到內用,用*代表的是必填字段內容。代碼也是很簡單的,咱們只是須要對參數進行復制就行了,使用$開頭。


1.2.4. 繪製方法模塊

繪製是全部內容之中的重頭戲,咱們單獨寫在一個js文件之中。繪製中包括自主繪製內容,子組件繪製內容,咱們須要分開來進行處理的。自主的內容繪製須要徹底繪製完成以後在調用子組件,不然的話並不保證樣式不會亂竄。(這也是很讓我頭痛的一點)。 應爲考量到相關的繪製延遲,因此我如今寫的代碼以下:

//主要是用於記錄須要延遲繪製的組件內容
const delays = []

//記錄最上層的繪製組件對象
let drawingOrigin = null

function drawingPath (layer, path) {
  // 當前爲空的狀況咱們能夠肯定當前繪製的是最上層組件的內容。
  if (!drawingOrigin) {
    drawingOrigin = layer
  }
  let brush = path || layer.path
  let used = false
  let i = 0
  const drawing = function () {
    // 判斷當前是否是有相關的imageData內容的處理函數。
    // 若是有而且當前組件並無更新的話,這直接使用imageData
    if (!layer.$update && layer.$imageData && layer.$putImageData) {
      layer.$putImageData(layer.context, layer.state, layer.$imageData)
    } else {
    
      // 調用path繪製方法。
      let autoDraw = brush.call(layer, layer.context, layer.state)
      
      // 下面這裏純屬爲了便捷,自主繪製而已。
      if (autoDraw) {
        if (layer.state.fill) {
          setBrushStyle(layer.context, layer.state.fill)
          layer.context.fill()
          used = true
        }
        if (layer.state.stroke) {
          if (used) {
            brush.call(layer, layer.context, layer.state)
          }
          setBrushStyle(layer.context, layer.state.stroke)
          layer.context.stroke()
        }
      }
      
      // 若是有getImageData方法的話(就是以前的imageData參數之中的get方法)
      // 則獲取新的imageData對象。
      if (layer.$getImageData) {
        layer.$imageData = layer.$getImageData(layer.context, layer.state)
      }
    }
  }
  
  // 探測到delay的話
  if (layer.$delay) {
  
    // 遍歷當前的delays內容,若是有當前的對象,則拿出來進行繪製。
    // 並從延遲隊列之中刪除它
    for (i = 0; i < delays.length; i++) {
      if (delays[i] === layer) {
        drawing()
        break
      }
    }
    if (i === delays.length) {
      delays.push(layer)
    } else {
      delays.splice(i, 1)
    }
  } else {
    drawing()
  }
  
  // 繪製完path之中的內容最終將會再次的檢測到最上層的組件元素.
  // 這個時候再講延遲元素拿出來繪製。
  if (drawingOrigin === layer) {
    drawingOrigin = null
    for (const val of delays) {
      drawingPath(val)
    }
  }
}
複製代碼

因此上面的代碼實際的繪製順序是,有限沒有延遲的組件和內容,延遲組件會進行存儲,在最後繪製,可是若是延時組件之中還有延時組件的話,會再一次的存儲到當前的延時組件隊列之中,在第一級延時組件繪製完成以後,在進行繪製,以此類推。


2.5. 動畫模塊

動畫模塊主要是對當前的內容進行循環的繪製,因此市場須要使用定時器,咱們須要對當前的定時器作一個兼容的操做,代碼以下:

function animateCompatible () {
  if (!window.requestAnimationFrame) {
    window.requestAnimationFrame =
      window.webkitRequestAnimationFrame ||
      function (callback) {
        return setTimeout(callback, 1000 / 60)
      }
  }
}
複製代碼

如上代碼所示,能夠解決咱們不少的不兼容的問題,覺得有的內容不支持當前新的requestAnimationFrame,固然這個函數將會讓咱們的繪製更爲的順暢。

animation之中嚴重影響咱們運行速率的是大量的定時器內容,以及每個定時器到時運行的時候的canvas的重繪,若是咱們針對動畫內容,每個動畫都給出一個定時器的話,實際上會浪費不少的動畫開銷的,由於相同時間間隔的動畫其實是能夠進行統一的繪製操做的,這樣不只減小了定時器的數量,也同時減小了canvas內容的重繪次數。會比較好的提高當前的動畫的效果。固然這是針對大量的定時以及繪製的狀況,少許的當前並無那麼多影響的。

那麼咱們能夠經過什麼方式來進行實現呢。將相同的動畫歸類到指定的時間間隔隊列之中,動畫間隔單次調用全部的當前時間間隔隊列之中的內容,統一的進行更新,可是針對不一樣時間的動畫內容,我並無肯定好更好的方式,如今能想到的是統一的心跳鍾,可是這樣的話,間隔心跳時長應該是多少ms,性能是否是真的能更近一步有待考較。這裏就不貼代碼了,由於方法很簡單,主要是動畫的開始和移除,還有添加等等操做,主要的內容就是時間間隔隊列內容。


2.6.事件交互模塊。

最後咱們須要處理的是事件交互模塊內容。事件也是canvas用來於用戶交互的關鍵所在,用戶在對canvas進行操做的時候咱們能夠獲取到相關的事件內容,而後斷定當前的位置信息,來肯定當前內容之中的組件信息。並觸發組件之中記錄的相同類型事件。事件的基礎處理函數內容是包括添加刪除事件還有觸發,添加刪除好理解,給定相關的事件類型,還有回調函數內容,以及用戶須要傳遞迴的meta參數(用戶須要在事件之中使用的參數,模塊只作存儲以及傳遞),添加到事件對象之中,或者在事件對象之中刪除掉某一類型的事件,或者事件類型下的某個回調函數。

事件的執行咱們是按照添加的順序進行執行的,實際上每個事件類型對應的都是一個回調函數隊列。emit函數內容咱們須要考量的主要是,出發事件的時候傳遞的內容,應該包括,以前的meta自定義參數,here的判斷結果,pos事件位置內容,以及最後的組件state參數內容。較全的參數數據將會使得事件內容更爲的靈活。代碼以下:

Layer.prototype.emit = function emit (type, pos) {
    const layer = this
    if (layer.$events[type]) {
      let list = layer.$events[type]
      for (let call of list) {
        let check = layer.$here && layer.$here(layer.state, pos.x, pos.y, layer.context)
        
        // 調用事件回調
        call.callback.call(layer, call.meta, check, pos, layer.state)
      }
    }
 }
複製代碼

最後還有一點,若是子組件添加事件的話,父組件須要添加相關的觸發函數,從而達到統一聯動的效果,因此在添加事件的時候須要斷定父組件之中是否有子組件當前時間類型的觸發函數,沒有的話須要加上。


總結

上述是我編寫的canvas模塊內容的version 1內容。只是一些粗淺的理解和想法。

version 2 之中我但願能夠作到: 1.動畫的統一心跳,或者再度優化。 2.imageData的可重用的擴展。 3.基礎組件元素的編寫集成。 4.事件這一塊內容可能須要更爲謹慎的對待,可是暫時尚未具體想法(思考狀ING) 5.配置化組件繪製,配置模塊將會提上日程。

但願還會有後續進度。 但願。。。。

相關文章
相關標籤/搜索