|
|
|
@ -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<number, Touch>) => void)=} onDown
|
|
|
|
|
* @prop {((touch: Touch[], touches: Map<number, Touch>) => void)=} onMove
|
|
|
|
|
* @prop {((touch: Touch | undefined, touches: Map<number, Touch>) => void)=} onUp
|
|
|
|
|
* @prop {number=} downscale
|
|
|
|
|
**/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {HTMLElement} element
|
|
|
|
|
* @param {AttachTouchOptions} options
|
|
|
|
|
**/
|
|
|
|
|
export function attachTouch(element, options = {}) {
|
|
|
|
|
/** @type {Map<PointerEvent['pointerId'], Touch>} **/
|
|
|
|
|
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,
|
|
|
|
|