为canvas内部元素添加事件

前言

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

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

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

本文参考了原作者在2016年写的canvas内部元素添加事件处理,感谢大佬分享。

自定义事件

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

复制代码
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

复制代码

在上面代码中,EventManager用来存储绑定了事件监听的对象,便于判断鼠标是否处于某个对象里面;EventTarget负责添加事件监听。

有序数组

在判断触发某个事件的元素时,需要遍历所有绑定了该事件的元素,判断鼠标位置是否位于元素内部。为了减少不必要的比较,这里使用了一个有序数组,使用元素区域的最小 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 {
  // 抽象类,该类继承了事件处理类,所有元素对象应该继承这个类
  // 为了实现对象比较,继承该类时应该同时实现compareTo, comparePointX 以及 hasPoint 方法。
  constructor () {
    super()
    this.canvas = null
    this.context = null
  }

  // 在有序数组中会根据这个方法的返回结果将对象排序
  compareTo (target) {
    return null
  }

  // 比较目标点的x值与当前区域的最小 x 值,结合有序数组使用,如果 point 的 x 小于当前区域的最小 x 值,那么有序数组中剩余
  // 元素的最小 x 值也会大于目标点的 x 值,就可以停止比较。在事件判断时首先使用该函数过滤一下。
  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

复制代码

收获

  1. 遇到问题别慌,该想的想想,该搜的搜一下
  2. 有些第三方库可能存在源码更新了但是文档还没更新的情况,这个时候就去github的issue中找找答案吧
  3. 原生javascript真的好美
  4. 感谢前人辛苦栽树(分享),我们才能更好的乘凉。

猜你喜欢

转载自juejin.im/post/7032580190798610468
今日推荐