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.配置化组件绘制,配置模块将会提上日程。

但愿还会有后续进度。 但愿。。。。

相关文章
相关标签/搜索