preface

I met such a requirement in my work this week: click a rectangle drawn on canvas, and a box will pop up to display the information related to this rectangle.

To be honest, at first I thought it was a good idea to bind a click event and pop up a box. When I went to write the code, I realized it wasn’t that simple. (I’m still too young)

When I bind the click event to canvas, I find THAT I can’t get the rectangle, I can only get some information of canvas, which makes me realize that the problem is not easy. After thinking for a while, I came up with a general idea that when we click on canvas, we should determine whether the area we click overlaps with the rectangle (whether it is within the rectangle area). If so, we should bind the corresponding events and perform some corresponding operations. Of course, to do this, you need custom events.

This article refers to the original author wrote in 2016 to add event handling inside canvas elements, thanks for sharing.

Custom events

import SortArray from './sortArray'

class EventManager {
  static _targets = {}
  // Get the corresponding listener function according to the event type
  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)
  }

  // Get the event prefix
  static _getPrefix (type) {
    if (type.indexOf('mouse')! = = -1) {
      return 'mouse'
    }

    if (type.indexOf('click')! = = -1) {
      return 'click'
    }
    return type
  }
}

export default EventManager

Copy the code
import EventManager from './eventManager'

class EventTarget {
  constructor () {
    this._listeners = {}
    this.inBounds = false
  }

  // Check whether an event is being listened on
  hasListener (type) {
    if (this._listeners.hasOwnProperty(type)) {
      return true
    } else {
      return false}}// Add a listener for the event
  addListener (type, listener) {
    if (!this._listeners.hasOwnProperty(type)) {
      this._listeners[type] = []
    }

    this._listeners[type].push(listener)
    EventManager.addTarget(type, this)}// Trigger the event
  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)
      }
    }
  }

  // If the listener is null, all event listeners for the current event are cleared
  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

Copy the code

In the above code, the EventManager is used to store objects that are bound to the event listener to determine whether the mouse is inside an object. The EventTarget is responsible for adding event listening.

Orderly array

To determine which element triggers an event, you need to iterate over all the elements bound to the event to determine whether the mouse position is inside the element. To reduce unnecessary comparisons, an ordered array is used, using the minimum X value of the element region as the comparison value, sorted in ascending order. If the minimum x value of an element region is greater than the x value of the mouse, there is no need to compare the elements following that element in the array.

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

Copy the code

Elements in the parent class

An abstract class is designed to be the parent of all element objects, inherits EventTarget, and defines three functions that all subclasses should implement.

import EventTarget from './eventTarget'

class DisplayObject extends EventTarget {
  // The abstract class that inherits the event handler class from which all element objects should inherit
  To implement object comparison, the class should be inherited with the compareTo, comparePointX, and hasPoint methods.
  constructor () {
    super(a)this.canvas = null
    this.context = null
  }

  // The objects in an ordered array are sorted based on the results of this method
  compareTo (target) {
    return null
  }

  // Compare the x value of the target point with the minimum X value of the current region, and use the ordered array. If the x value of the point is less than the minimum X value of the current region, then the ordered array is left
  // The element's minimum x value will also be greater than the target point's x value, and the comparison can be stopped. This function is first used to filter events.
  comparePointX (point) {
    return null
  }

  // Check whether the target point is in the current region
  hasPoint (point) {
    return false}}export default DisplayObject

Copy the code

events

Take mouse events as an example. Here we implement mouseover, Mousemove, and Mouseout mouse events. First, add a mouseover event to canvas. When the mouse moves on canvas, it will compare the current mouse position with the position of the element bound with the above three events. If triggering conditions are met, the fire method of the element will be called to trigger the corresponding event.

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 is passed here primarily to use the _windowToCanvas function
    const point = container._windowToCanvas(event.clientX, event.clientY)

    const array = EventManager.getTargets('mouse')

    if(array ! =null) {
      array.search(point)
      // The element where the mouse is located
      const selectedElements = array.selectedElements
      // The mouse is not there
      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

Copy the code

harvest

  1. Don’t panic when you encounter a problem. Think about it and search it
  2. Some third-party libraries may have the source code updated but the documentation has not yet been updated. In this case, check out the Github issue for answers
  3. Native javascript is beautiful
  4. Thanks to the predecessors hard to plant trees (share), we can better enjoy the shade.