I wanted to try out pointer events with Vue.
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.
The @touchstart.prevent will prevent the drag triggering scrolling on mobile devices. If the drag handle has text, you might want @pointerdown.prevent to prevent double click from selecting the text. You may also want a few other things, such as @dragstart.prevent and CSS user-select:none, depending on the situation. See my draggable object guide.
Vue's computed
with setters are useful here. I can go from model coordinates to svg/canvas coordinates using the get()
part of the computed, and then in reverse, svg/canvas coordinates to model coordinates using the set()
part of the computed.
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}, }; }, template: `<canvas v-bind:width="width" v-bind:height="height" v-on:touchstart.prevent="" v-on:pointerdown="pointerdown" v-on:pointerup="pointerup" v-on:pointercancel="pointerup" v-on="dragging? {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) { this.end = this.eventToCanvasCoordinate(event); this.redraw(); }, pointerdown(event) { const target = event.target; // 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" v-bind:style="{cursor: dragging? 'grab' : 'grabbing'}" v-on:touchstart.prevent="" v-on:pointerdown="pointerdown" v-on:pointerup="pointerup" v-on:pointercancel="pointerup" v-on="dragging? {pointermove} : {}" />`, 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) { this.$emit('move', this.eventToCanvasCoordinate(event)); }, pointerdown(event) { const target = event.target; // 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');