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 is based on a blog post written by the author in 2016. Thanks for sharing.
Custom events
In order to implement custom javascript object events, we can create a management object, the object contains an internal object (used as a map, the event name as the attribute name, event processing functions as attribute values, because there may be multiple event handlers, so use an array to store event handler), storage related events.
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)} 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; Listeners [I]. Call (this, event)}}} 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)} // Obtain the event prefix static _getPrefix (type) { if (type.indexOf('mouse') ! == -1) { return 'mouse' } if (type.indexOf('click') ! == -1) { return 'click' } return type }}export default EventManagerCopy the code
In the code above,EventManager
Store all objects that are bound to event listeners so that you can later determine if the mouse is inside an object.
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 SortArrayCopy 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 { constructor () { super() this.canvas = null this.context = null } compareTo (target) { return null } comparePointX (point) { return null } 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) {const point = container._windowToCanvas(event.clientx, event.clientY) const array = EventManager.getTargets('mouse') if (array ! Const selectedElements = array.selectedelements // Const selectedElements where the mouse is not present 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 ContainerCopy the code