[HTML5] add event handling to Canvas internal elements

Posted by Ruski on Wed, 02 Mar 2022 10:56:21 +0100

preface

Canvas does not provide a method to add event listening for its internal elements, so if you want to make the elements in canvas respond to events, you need to implement it yourself. The implementation method is also very simple. First, obtain the coordinates of the mouse on the canvas, calculate which elements the current coordinates are inside, and then operate the elements accordingly. With custom events, we can add event listening effect for elements in canvas.

Source code demonstration

Custom event

In order to realize the custom events of javascript objects, we can create an object to manage events, which contains an internal object (used as a map, the event name as the attribute name, and the event handling function as the attribute value. Because there may be multiple event handling functions, we use the array to store the event handling function) to store related events. Then provide a function to trigger the event, and call the previously bound function by using the call method. Here is a code example:

(function () {
    cce.EventTarget = function () {

        this._listeners = {};
        this.inBounds = false;

    };

    cce.EventTarget.prototype = {
        constructor: cce.EventTarget,

        // Check whether an event is monitored
        hasListener: function (type) {
            if (this._listeners.hasOwnProperty(type)) {
                return true;
            } else {
                return false;
            }
        },

        // Add listener function for event
        addListener: function (type, listener) {
            if (!this._listeners.hasOwnProperty(type)) {
                this._listeners[type] = [];
            }

            this._listeners[type].push(listener);
            cce.EventManager.addTarget(type, this);
        },

        // Trigger event
        fire: function (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 listener is null, all event listeners under the current event will be cleared
        removeListener: function (type, listener) {
            if (listener == null) {
                if (this._listeners.hasOwnProperty(type)) {
                    this._listeners[type] = [];
                    cce.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)
                            cce.EventManager.removeTarget(type, this);
                        break;
                    }
                }
            }

        }
    };
}());

In the above code, the EventManager is used to store all objects bound to event listening, so as to judge whether the mouse is inside an object later. If a custom object needs to add event listener, it only needs to inherit EventTarget.

Ordered array

When judging the element that triggers an event, you need to traverse all the elements bound to the event to judge whether the mouse position is inside the element. In order to reduce unnecessary comparisons, an ordered array is used here, which uses the minimum x value of the element area as the comparison value and is arranged in ascending order. If the minimum x value of an element area is greater than the x value of the mouse, there is no need to compare the elements after the element in the array. The specific implementation can be seen SortArray.js

Element parent

An abstract class is designed here as the parent class of all element objects. This class inherits EventTarget and defines three functions. All subclasses should implement these three functions. The specific code is as follows:

(function () {

    // Abstract class, which inherits the event handling class. All element objects should inherit this class
    // In order to realize object comparison, compareTo, comparePointX and hasPoint methods should be implemented when inheriting this class.
    cce.DisplayObject = function () {
        cce.EventTarget.call(this);
        this.canvas = null;
        this.context = null;

    };

    cce.DisplayObject.prototype = Object.create(cce.EventTarget.prototype);
    cce.DisplayObject.prototype.constructor = cce.DisplayObject;

    // In the ordered array, the objects will be sorted according to the returned results of this method
    cce.DisplayObject.prototype.compareTo = function (target) {
        return null;
    };

    // Compare the x value of the target point with the minimum x value of the current area, and use it in combination with the ordered array. If the x value of the point is less than the minimum x value of the current area, then the remaining x value in the ordered array
    // The minimum x value of the element will also be greater than the x value of the target point, and the comparison can be stopped. When judging events, first use this function to filter.
    cce.DisplayObject.prototype.comparePointX = function (point) {
        return null;
    };

    // Judge whether the target point is in the current area
    cce.DisplayObject.prototype.hasPoint = function (point) {
        return false;
    };

}());

Event judgment

Taking mouse events as an example, here we implement three mouse events: mouseover, MouseMove and mouseout. First, add a mouseover event to the canvas. When the mouse moves on the canvas, it will always compare the current mouse position with the position of the element bound to the above three events. If the trigger conditions are met, call the fire method of the element to trigger the corresponding event. Here is the sample code:

_handleMouseMove: function (event, container) {

    // The container is passed in here for use_ windowToCanvas function
    var point = container._windowToCanvas(event.clientX, event.clientY);

    // Get the element object bound with mouseover, MouseMove and mouseout events
    var array = cce.EventManager.getTargets("mouse");
    if (array != null) {
        array.search(point);

        // The element where the mouse is located
        var selectedElements = array.selectedElements;

        // Elements without mouse
        var unSelectedElements = array.unSelectedElements;
        selectedElements.forEach(function (ele) {
            if (ele.hasListener("mousemove")) {
                var event = new cce.Event(point.x, point.y, "mousemove", ele);
                ele.fire("mousemove", event);
            }

            // It was not in the area before, but now it is, indicating that the mouse has entered
            if (!ele.inBounds) {
                ele.inBounds = true;
                if (ele.hasListener("mouseover")) {
                    var event = new cce.Event(point.x, point.y, "mouseover", ele);
                    ele.fire("mouseover", event);
                }
            }
        });

        unSelectedElements.forEach(function (ele) {

            // It was in the area before, but now it's gone, indicating that the mouse has left
            if (ele.inBounds) {
                ele.inBounds = false;
                if (ele.hasListener("mouseout")) {
                    var event = new cce.Event(point.x, point.y, "mouseout", ele);
                    ele.fire("mouseout", event);
                }
            }
        });
    }
}

other

Execute function now

Functions such as the following form are called immediate execution functions.

(function() {
    // code
}());

The advantage of using the immediate execution function is that it limits the scope of variables, so that the variables defined in the immediate execution function will not pollute other scopes. For more detailed explanation, please see here

apply, call, bind

The use of these three functions is similar to the method in java reflection Invoke: the method, as a body, passes the object executing the method into the method as a parameter. The functions of apply and call are the same. They will be executed immediately after the call, but the forms of accepting parameters are different.

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])

bind will return the corresponding function, and it will not be executed immediately, so it will be easy to call later. Take the following example:

function aa() {
    console.log(111);
    console.log(this);
}

var bb = aa.bind(Math);
bb();

For more detailed explanation, please see here

addEventListener parameter

If you need to pass parameters when adding event listening to an element, you can use the following method

var i = 1;
aa.addEventListener("click", function() {
    bb(i);
}, false);

Call the constructor of the parent class

Just use call

Child = function() {
    Parent.call(this);
}

Object detection

Determine whether the object is null or undefined

Determine whether an object has a property

isPointInPath

Whether the point is inside a path in canvas can be used for polygon detection. However, the path used by isPointInPath is the last graph drawn. If there are multiple graphs that need to be judged, the previous graph path needs to be saved. When judging, the path needs to be reconstructed, but it does not need to be drawn, as shown below

this.context.save();
this.context.beginPath();

//console.log(this.points);
this.context.moveTo(this.points[0].x, this.points[0].y);

for (var i = 1; i < this.points.length; i++) {
    this.context.lineTo(this.points[i].x, this.points[i].y);
}

if (this.context.isPointInPath(target.x, target.y)) {
    isIn = true;
}
this.context.closePath();
this.context.restore();

Reference article: