Vue pointer events

 from Red Blob Games
5 May 2022

I wanted to try out pointer events with Vue.

Demo: mark a rectangle, or drag the circle

The idea is that on pointerdown I capture pointer events. It will automatically release the capture on pointerup. While the pointer is down I want pointermove to update the rectangle position.

I tried with canvas and svg because the pointer position code is slightly different for the two. vue-pointerevents.js :

const app = createApp({
    data() { return {p: {x: 0, y: 0}}; },
    methods: {
        clamp(value, lo, hi) {
            if (value < lo) value = lo;
            if (value > hi) value = hi;
            return value;
        },
    },
});

// Here's an example with <canvas>
app.component('mark-rectangle', {
    props: {
        width: Number,
        height: Number,
    },
    data() {
        return {
            dragging: false,
            begin: {x: 100, y: 100},
            end: {x: 200, y: 200},
        };
    },
    // touch-action: none is for touch devices - want to capture pointer and not scroll page
    template: `<canvas v-bind:width="width" v-bind:height="height"
                       style="touch-action: none"
                       v-on:pointerdown="pointerdown" v-on:pointerup="pointerup" v-on:pointermove="pointermove">
               </canvas>`,
    mounted() {
        this.redraw();
    },
    methods: {
        redraw() {
            let ctx = this.$el.getContext('2d');
            ctx.clearRect(0, 0, this.width, this.height);
            ctx.fillStyle = this.dragging ? "hsl(200 50% 50% / 30%)" : "hsl(200 50% 50% / 50%)";
            ctx.strokeStyle = "black";
            ctx.beginPath();
            ctx.rect(this.begin.x, this.begin.y,
                     this.end.x - this.begin.x, this.end.y - this.begin.y);
            ctx.fill();
            ctx.stroke();
            ctx.fillStyle = "black";
            ctx.fillRect(this.begin.x - 1, this.begin.y - 1, 3, 3);
            ctx.fillRect(this.end.x - 2, this.end.y - 2, 3, 3);
        },
        eventToCanvasCoordinate(event) {
            // need to use getBoundingClientRect for responsive <canvas> sizing
            // NOTE: if you use transforms, see
            // https://stackoverflow.com/a/59259174
            // to invert transform matrix
            let canvas = this.$el;
            let bounds = canvas.getBoundingClientRect();
            let x = event.x - bounds.left;
            let y = event.y - bounds.top;
            x = x / bounds.width * canvas.width;
            y = y / bounds.height * canvas.height;
            return {x, y};
        },
        pointermove(event) {
            if (!this.dragging) return;
            this.end = this.eventToCanvasCoordinate(event);
            this.redraw();
        },
        pointerdown(event) {
            const target = event.target;
            event.preventDefault();
            event.stopPropagation();
            // we want all the events until the pointer is released
            target.setPointerCapture(event.pointerId);
            this.dragging = true;
            this.begin = this.eventToCanvasCoordinate(event);
            this.redraw();
        },
        pointerup(event) {
            this.pointermove(event);
            this.dragging = false;
            this.redraw();
        },

    },
    
});

// Here's an example with <svg>
app.component('a-point', {
    props: ['at'],
    emits: ['move'],
    template: `<circle v-bind:cx="at.x" v-bind:cy="at.y" v-bind:r="5" fill="red"
                style="touch-action:none;cursor:hand"
                v-on:pointerdown="pointerdown"
                v-on:pointermove="pointermove"
                v-on:pointerup="pointerup" />`,
    data() { return {dragging: false}; },
    methods: {
        eventToCanvasCoordinate(event) {
            const svg = this.$el.ownerSVGElement;
            // NOTE: svg.getScreenCTM already factors in the bounding rect
            // so there's no need to subtract rect, or even call getBoundingClientRect
            let point = svg.createSVGPoint();
            point.x = event.clientX;
            point.y = event.clientY;
            let coords = point.matrixTransform(svg.getScreenCTM().inverse());
            return coords;
        },
        pointermove(event) {
            if (!this.dragging) return;
            this.$emit('move', this.eventToCanvasCoordinate(event));
        },
        pointerdown(event) {
            const target = event.target;
            event.preventDefault();
            event.stopPropagation();
            // we want all the events until the pointer is released
            target.setPointerCapture(event.pointerId);
            this.dragging = true;
            this.pointermove(event);
        },
        pointerup(event) {
            this.pointermove(event);
            this.dragging = false;
        },
    },
});


app.mount('figure');

Email me , or tweet @redblobgames, or comment: