From 6aa87d052a1a7244795c363e42c1eac727ed4dc3 Mon Sep 17 00:00:00 2001 From: Adrien Burgun Date: Wed, 22 Feb 2023 17:04:41 +0100 Subject: [PATCH] :sparkles: Add PixelPerfectTouch --- package.json | 2 +- src/index.js | 155 ++++++++++++++++++++++++++++++++ src/solid/PixelPerfectTouch.tsx | 28 ++++++ src/solid/index.tsx | 1 + 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/solid/PixelPerfectTouch.tsx diff --git a/package.json b/package.json index 845c358..8a388f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shadryx/pptk", - "version": "0.1.2", + "version": "0.1.3", "description": "Pixel-Perfect ToolKit, a library to help make pixel-perfect applications on high-DPI devices", "keywords": [ "pixel-perfect", diff --git a/src/index.js b/src/index.js index 34b4f00..21ed3f0 100644 --- a/src/index.js +++ b/src/index.js @@ -157,6 +157,161 @@ export function getContext2D(canvas) { return new PixelPerfectContext2D(canvas); } +/** + * @typedef {object} Touch + * @prop {number} x + * @prop {number} y + * @prop {number} click_x + * @prop {number} click_y + * @prop {number} dx + * @prop {number} dy + * @prop {PointerEvent['pointerType']} type + * @prop {number} id + **/ + +/** + * @typedef {object} AttachTouchOptions + * @prop {((touch: Touch, touches: Map) => void)=} onDown + * @prop {((touch: Touch[], touches: Map) => void)=} onMove + * @prop {((touch: Touch | undefined, touches: Map) => void)=} onUp + * @prop {number=} downscale + **/ + +/** + * @param {HTMLElement} element + * @param {AttachTouchOptions} options + **/ +export function attachTouch(element, options = {}) { + /** @type {Map} **/ + const touches = new Map(); + + const downscale = options.downscale ?? 1; + + let queued = null; + let queued_touches = []; + + function get_dpr() { + if (options.dpr) { + return window.devicePixelRatio ?? 1; + } else { + return 1; + } + } + + function onDown(event) { + if (options.preventDefault) event.preventDefault(); + + let dpr = get_dpr(); + let bounding = element.getBoundingClientRect(); + + let x = (event.clientX - bounding.x) * dpr / downscale; + let y = (event.clientY - bounding.y) * dpr / downscale; + + let touch = { + x, + y, + click_x: x, + click_y: y, + dx: 0, + dy: 0, + type: event.pointerType, + id: event.pointerId, + }; + + touches.set(event.pointerId, touch); + + options.onDown?.(touch, touches); + } + + function onMove(event) { + if (options.preventDefault) event.preventDefault(); + + let dpr = get_dpr(); + let bounding = element.getBoundingClientRect(); + + let x = (event.clientX - bounding.x) * dpr / downscale; + let y = (event.clientY - bounding.y) * dpr / downscale; + + if (touches.has(event.pointerId)) { + let touch = touches.get(event.pointerId); + + touch.dx = x - touch.x; + touch.dy = y - touch.y; + + touch.x = x; + touch.y = y; + + if (queued === null) { + queued = setTimeout(() => { + try { + options.onMove?.(queued_touches, touches); + } finally { + queued_touches = []; + queued = null; + } + }, 0); + } + queued_touches.push(touch); + } + } + + function onUp(event) { + if (options.preventDefault) event.preventDefault(); + + let dpr = get_dpr(); + let bounding = element.getBoundingClientRect(); + + let x = (event.clientX - bounding.x) * dpr / downscale; + let y = (event.clientY - bounding.y) * dpr / downscale; + + let touch = touches.get(event.pointerId); + + if (touch) { + touch.dx = x - touch.x; + touch.dy = y - touch.y; + + touch.x = x; + touch.y = y; + } + + touches.delete(event.pointerId); + + options.onUp?.(touch, touches); + } + + function onCancel(event) { + const touch = touches.get(event.pointerId); + touches.delete(event.pointerId); + + options.onUp?.(touch, touches); + } + + element.addEventListener("pointerdown", onDown); + element.addEventListener("pointermove", onMove); + element.addEventListener("pointerup", onUp); + element.addEventListener("pointercancel", onCancel); + + function ontouchmove(event) { + event.preventDefault(); + } + + // preventDefault() on pointer events does not work as expected (eg. scrolling still happens), so this is needed instead + if (options.preventDefault) { + element.addEventListener("touchmove", ontouchmove, {passive: false}); + } + + return function unbind() { + element.removeEventListener("pointerdown", onDown); + element.removeEventListener("pointermove", onMove); + element.removeEventListener("pointup", onUp); + element.removeEventListener("pointercancel", onCancel); + + if (options.preventDefault) { + element.removeEventListener("touchmove", ontouchmove, {passive: false}); + } + } +} + export const StrokeMode = Object.freeze({ DEFAULT: 0, INSIDE: 1, diff --git a/src/solid/PixelPerfectTouch.tsx b/src/solid/PixelPerfectTouch.tsx new file mode 100644 index 0000000..5510fcf --- /dev/null +++ b/src/solid/PixelPerfectTouch.tsx @@ -0,0 +1,28 @@ +import { children, Component, createEffect, JSX, onCleanup } from "solid-js"; +import { attachTouch, AttachTouchOptions, Touch } from "../index.js"; + +export type TouchHandler = (touches: Map, touch: T) => void; + +export type PixelPefectTouchProps = { + children: JSX.Element; + + onMount?: (element: HTMLDivElement) => void; +} & AttachTouchOptions; + +export const PixelPerfectTouch: Component = ( + { children, onMount, ...options } +) => { + let element: HTMLDivElement; + + createEffect(() => { + onMount?.(element); + + if (!element) return; + + const cleanup = attachTouch(element, options); + + onCleanup(cleanup); + }); + + return
element = el}>{children}
; +}; diff --git a/src/solid/index.tsx b/src/solid/index.tsx index 17d0d10..f98e213 100644 --- a/src/solid/index.tsx +++ b/src/solid/index.tsx @@ -1 +1,2 @@ export * from "./PixelPerfectCanvas.jsx"; +export * from "./PixelPerfectTouch.jsx";