前言
本周在工作中遇到了这样一个需求:点击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
复制代码