为canvas内的元素添加事件回调

前言

        本周在工作中遇到了这样一个需求:点击canvas画出来的矩形,要弹出一个框来显示这个矩形相关的信息。

        说真的,我一开始觉得这个还挺好做的呀,不就是绑定一个click事件,然后弹出来一个框么?当我去写代码的时候我发现事情不是这样简单的。(我还是太年轻啊)

        当我给canvas绑定点击事件以后,我发现我拿不到矩形,我只能拿到canvas这个画布的一些信息,这下我意识到问题不简单啊。我思考了一会儿,想了一个大致的思路是当我们点击canvas的时候,判断我们点击的区域和矩形是不是重叠(是不是在矩形区域内),如果是的话,我们就绑定对应的事件做出一些相应的操作。当然,要实现这个想法,这里就要用到自定义事件了。

本文参考了原作者在2016年写的博文,感谢大佬分享。

自定义事件

         为了实现javascript对象的自定义事件,我们可以创建一个管理事件的对象,该对象中包含一个内部对象(当作map使用,事件名作为属性名,事件处理函数作为属性值,因为可能有个多个事件处理函数,所以使用数组存储事件处理函数),存储相关的事件。

import EventManager from './eventManager'class EventTarget {  constructor () {    this._listeners = {}    this.inBounds = false  }  // 查看某个事件是否有监听  hasListener (type) {    if (this._listeners.hasOwnProperty(type)) {      return true    } else {      return false    }  }  // 为事件添加监听函数  addListener (type, listener) {    if (!this._listeners.hasOwnProperty(type)) {      this._listeners[type] = []    }    this._listeners[type].push(listener)    EventManager.addTarget(type, this)  }  // 触发事件  fire (type, event) {    if (event == null || event.type == null) {      return    }    if (this._listeners[event.type] instanceof Array) {      var listeners = this._listeners[event.type]      for (var i = 0, len = listeners.length; i < len; i++) {        listeners[i].call(this, event)      }    }  }  // 如果listener 为null,则清除当前事件下的全部事件监听  removeListener (type, listener) {    if (listener == null) {      if (this._listeners.hasOwnProperty(type)) {        this._listeners[type] = []        EventManager.removeTarget(type, this)      }    }    if (this._listeners[type] instanceof Array) {      var listeners = this._listeners[type]      for (var i = 0, len = listeners.length; i < len; i++) {        if (listeners[i] === listener) {          listeners.splice(i, 1)          if (listeners.length === 0) EventManager.removeTarget(type, this)          break        }      }    }  }}export default EventTarget

import SortArray from './sortArray'class EventManager {  static _targets = {}  // 根据事件类型拿到对应的监听函数  static getTargets (type) {    if (type === null) {      return    }    type = this._getPrefix(type)    return this._targets[type]  }  static addTarget (type, target) {    if (type === null) {      return    }    type = this._getPrefix(type)    if (!this._targets.hasOwnProperty(type)) {      this._targets[type] = new SortArray()    }    const array = this._targets[type]    if (!array.contains(target)) {      array.add(target)    }  }  static removeTarget (type, target) {    if (type == null) {      return    }    type = this._getPrefix(type)    if (!this._targets.hasOwnProperty(type)) {      return    }    var array = this._targets[type]    array.delete(target)  }  // 获取事件前缀  static _getPrefix (type) {    if (type.indexOf('mouse') !== -1) {      return 'mouse'    }    if (type.indexOf('click') !== -1) {      return 'click'    }    return type  }}export default EventManager 
复制代码

在上面的代码中,EventManager 用来存储所有绑定了事件监听的对象,便于后面判断鼠标是否位于某个对象内部。

有序数组

        在判断触发某个事件的元素时,需要遍历所有绑定了该事件的元素,判断鼠标位置是否位于元素内部。为了减少不必要的比较,这里使用了一个有序数组,使用元素区域的最小 x 值作为比较值,按照升序排列。如果一个元素区域的最小 x 值大于鼠标的 x 值,那么就无需比较数组中该元素后面的元素。

class SortArray {  constructor () {    this._data = []    this.selectedElements = []    this.unSelectedElements = []  }  add (ele) {    if (ele == null) {      return    }    let i, data, index, result    for (i = 0, index = 0; i < this._data.length; i++) {      data = this._data[i]      result = ele.compareTo(data)      if (result == null) {        return      }      if (result > 0) {        index++      } else {        break      }    }    for (i = this._data.length; i > index; i--) {      this._data[i] = this._data[i - 1]    }    this._data[index] = ele  }  contains (ele) {    if (ele == null) {      return false    }    let low, mid, high    low = 0    high = this._data.length - 1    while (low <= high) {      mid = parseInt((low + high) / 2)      if (this._data[mid] === ele) {        return true      }      if (this._data[mid].compareTo(ele) < 0) {        low = mid + 1      } else {        high = mid - 1      }    }    return false  }  search (point) {    let d    this.selectedElements.length = 0    this.unSelectedElements.length = 0    for (var i = 0; i < this._data.length; i++) {      d = this._data[i]      if (d.comparePointX(point) > 0) {        break      }      if (d.hasPoint(point)) {        this.selectedElements.push(d)      } else {        this.unSelectedElements.push(d)      }    }    for (; i < this._data.length; i++) {      d = this._data[i]      this.unSelectedElements.push(d)    }  }  print () {    this._data.forEach(function (data) {      console.log(data)    })  }  delete (ele) {    var index = -1    for (var i = 0; i < this._data.length; i++) {      if (ele === this._data[i]) {        index = i        break      }    }    this._data.splice(index, 1)  }  reset () {    this._data.length = 0    this.selectedElements.length = 0    this.unSelectedElements.length = 0  }}export default SortArray
复制代码

 元素父类

这里设计了一个抽象类,来作为所有元素对象的父类,该类继承了 EventTarget,并且定义了三个函数,所有子类都应该实现这三个函数。 

import EventTarget from './eventTarget'class DisplayObject extends EventTarget {  constructor () {    super()    this.canvas = null    this.context = null  }  compareTo (target) {    return null  }  comparePointX (point) {    return null  }  hasPoint (point) {    return false  }}export default DisplayObject
复制代码

事件判断

以鼠标事件为例,这里我们实现了 mouseover, mousemove, mouseout 三种鼠标事件。首先对 canvas 添加 mouseover事件,当鼠标在 canvas 上移动时,会时时对比当前鼠标位置与绑定了上述三种事件的元素的位置,如果满足了触发条件就调用元素的 fire方法触发对应的事件。

import EventManager from './eventManager'import CustomEvent from './event'class Container {  constructor (canvas) {    if (canvas === null) {      throw Error("canvas can't be null")    }    this.canvas = canvas    this.context = this.canvas.getContext('2d')    this._childs = []  }  addChild (displayObject) {    displayObject.canvas = this.canvas    displayObject.context = this.context    this._childs.push(displayObject)  }  draw () {    this._childs.forEach(child => {      child.draw()    })  }  enableMouse () {    this.canvas.addEventListener(      'mousemove',      event => {        this._handleMouseMove(event, this)      },      false    )  }  enableClick () {    this.canvas.addEventListener(      'click',      event => {        this._handleClick(event, this)      },      false    )  }  _handleMouseMove (event, container) {    // 这里传入container 主要是为了使用 _windowToCanvas函数    const point = container._windowToCanvas(event.clientX, event.clientY)    const array = EventManager.getTargets('mouse')    if (array != null) {      array.search(point)      // 鼠标所在的元素      const selectedElements = array.selectedElements      // 鼠标不在的元素      const unSelectedElements = array.unSelectedElements      selectedElements.forEach(function (ele) {        if (ele.hasListener('mousemove')) {          const customEvent = new CustomEvent(            point.x,            point.y,            'mousemove',            ele          )          ele.fire('mousemove', customEvent)        }        if (!ele.inBounds) {          ele.inBounds = true          if (ele.hasListener('mouseover')) {            const event = new CustomEvent(point.x, point.y, 'mouseover', ele)            ele.fire('mouseover', event)          }        }      })      unSelectedElements.forEach(function (ele) {        if (ele.inBounds) {          ele.inBounds = false          if (ele.hasListener('mouseout')) {            var event = new CustomEvent(point.x, point.y, 'mouseout', ele)            ele.fire('mouseout', event)          }        }      })    }  }  _handleClick (event, target) {    const point = target._windowToCanvas(event.clientX, event.clientY)    const array = EventManager.getTargets('click')    if (array !== null) {      array.search(point)      var selectedElements = array.selectedElements      selectedElements.forEach(function (ele) {        if (ele.hasListener('click')) {          var event = new CustomEvent(point.x, point.y, 'click', ele)          ele.fire('click', event)        }      })    }  }  _windowToCanvas (x, y) {    const bbox = this.canvas.getBoundingClientRect()    return {      x: x - bbox.left,      y: y - bbox.top    }  }}export default Container
复制代码

猜你喜欢

转载自juejin.im/post/7032461498589020173