Mouse.js

/**
 * A NetLogo-like mouse handler.
 *
 * **TODO: Example usage.**
 */
class Mouse {
    /**
     * Create and start mouse obj, args: a model, and a callback method.
     *
     * @param {Canvas|String} canvas The canvas upon which we track the mouse.
     *      If a string is given, get the HTML element by that name
     * @param {world} world World instance
     * @param {Function} callback callback(evt, mouse) called on every mouse action
     */
    constructor(canvas, world, callback = (evt, mouse) => {}) {
        if (typeof canvas === 'string') {
            canvas = document.getElementById(canvas)
        }
        Object.assign(this, { canvas, world, callback })

        // instance event handlers: arrow fcns to insure "this" is us.
        // I.e. doesn't work to just use handleXXX in addEventListener.
        this.mouseDown = e => this.handleMouseDown(e)
        this.mouseUp = e => this.handleMouseUp(e)
        this.mouseMove = e => this.handleMouseMove(e)
    }

    resetParams() {
        this.x = this.y = NaN
        this.moved = this.down = false
    }

    /**
     * Start/stop the mouseListeners.  Note that NetLogo's model is to have
     * mouse move events always on, rather than starting/stopping them
     * on mouse down/up.  We may want do make that optional, using the
     * more standard down/up enabling move events.
     * @returns this Return this instance for chaining
     */
    start() {
        // Note: multiple calls safe
        this.canvas.addEventListener('mousedown', this.mouseDown)
        document.body.addEventListener('mouseup', this.mouseUp)
        this.canvas.addEventListener('mousemove', this.mouseMove)
        this.resetParams()
        return this // chaining
    }
    stop() {
        // Note: multiple calls safe
        this.canvas.removeEventListener('mousedown', this.mouseDown)
        document.body.removeEventListener('mouseup', this.mouseUp)
        this.canvas.removeEventListener('mousemove', this.mouseMove)
        this.resetParams()
        return this // chaining
    }

    get running() {
        return !isNaN(this.x)
    }
    run(on = true) {
        if (on) this.start()
        else this.stop()
    }
    // set run(on = true) {
    //     if (on) this.start()
    //     else this.stop()
    // }
    // toggle() {
    //     if (isNaN(this.x)) this.start()
    //     else this.stop()
    // }

    // Handlers for eventListeners
    generalHandler(e, down, moved) {
        this.down = down
        this.moved = moved
        this.setXY(e)
        this.callback(this, e) // callback generally doesn't use e
    }
    handleMouseDown(e) {
        this.action = 'down'
        this.generalHandler(e, true, false)
    }
    handleMouseUp(e) {
        this.action = 'up'
        this.generalHandler(e, false, false)
    }
    handleMouseMove(e) {
        this.action = this.down ? 'drag' : 'move'
        this.generalHandler(e, this.down, true)
    }

    // Event locations, clientX/Y, screenX/Y, offsetX/Y, pageX/Y .. confusing!
    // Stack Overflowhttps://tinyurl.com/y5k9rwhb

    // set x, y to be event location in turtle coordinates, floats.
    setXY(e) {
        const { canvas, world } = this
        const patchSize = world.patchSize(canvas)
        const rect = this.canvas.getBoundingClientRect()
        const pixX = e.clientX - rect.left
        const pixY = e.clientY - rect.top

        // const [x, y] = world.pixelXYtoPatchXY(pixX, pixY, patchSize)
        // Object.assign(this, { x, y })
        ;[this.x, this.y] = world.pixelXYtoPatchXY(pixX, pixY, patchSize)
    }
}

export default Mouse