From f396b4a16ab3500da038ade6859f73ecbf7d2f7f Mon Sep 17 00:00:00 2001 From: Grant Date: Tue, 13 Feb 2024 16:30:49 -0700 Subject: [PATCH] move mouse events to PanZoom --- .../src/components/CanvasWrapper.tsx | 13 +- packages/client-next/src/lib/canvas.ts | 107 ++++++++-------- packages/lib/src/renderer/PanZoom.ts | 115 ++++++++++++++++-- 3 files changed, 162 insertions(+), 73 deletions(-) diff --git a/packages/client-next/src/components/CanvasWrapper.tsx b/packages/client-next/src/components/CanvasWrapper.tsx index 56a6da9..376afca 100644 --- a/packages/client-next/src/components/CanvasWrapper.tsx +++ b/packages/client-next/src/components/CanvasWrapper.tsx @@ -1,14 +1,8 @@ -import React, { createRef, useEffect } from "react"; -import { - TransformComponent, - TransformWrapper, - useControls, - useTransformEffect, -} from "react-zoom-pan-pinch"; +import React, { createRef, useContext, useEffect } from "react"; import { Canvas } from "../lib/canvas"; import { useAppContext } from "../contexts/AppContext"; -import throttle from "lodash.throttle"; import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer"; +import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext"; export const CanvasWrapper = () => { // to prevent safari from blurring things, use the zoom css property @@ -24,6 +18,7 @@ export const CanvasWrapper = () => { const CanvasInner = () => { const canvasRef = createRef(); const { config } = useAppContext(); + const PanZoom = useContext(RendererContext); // const { centerView } = useControls(); // useTransformEffect( @@ -39,7 +34,7 @@ const CanvasInner = () => { useEffect(() => { if (!config.canvas || !canvasRef.current) return; const canvas = canvasRef.current!; - const canvasInstance = new Canvas(config, canvas); + const canvasInstance = new Canvas(config, canvas, PanZoom); // centerView(); return () => { diff --git a/packages/client-next/src/lib/canvas.ts b/packages/client-next/src/lib/canvas.ts index f5d1f9d..284a351 100644 --- a/packages/client-next/src/lib/canvas.ts +++ b/packages/client-next/src/lib/canvas.ts @@ -1,6 +1,11 @@ import EventEmitter from "eventemitter3"; import { ClientConfig, IPalleteContext, Pixel } from "../types"; import Network from "./network"; +import { + ClickEvent, + HoverEvent, + PanZoom, +} from "@sc07-canvas/lib/src/renderer/PanZoom"; export class Canvas extends EventEmitter { static instance: Canvas | undefined; @@ -8,6 +13,7 @@ export class Canvas extends EventEmitter { private _destroy = false; private config: ClientConfig; private canvas: HTMLCanvasElement; + private PanZoom: PanZoom; private ctx: CanvasRenderingContext2D; private cursor = { x: -1, y: -1 }; @@ -16,24 +22,27 @@ export class Canvas extends EventEmitter { } = {}; private lastPlace: number | undefined; - constructor(config: ClientConfig, canvas: HTMLCanvasElement) { + constructor( + config: ClientConfig, + canvas: HTMLCanvasElement, + PanZoom: PanZoom + ) { super(); Canvas.instance = this; this.config = config; this.canvas = canvas; + this.PanZoom = PanZoom; this.ctx = canvas.getContext("2d")!; canvas.width = config.canvas.size[0]; canvas.height = config.canvas.size[1]; - canvas.addEventListener("mousemove", this.handleMouseMove.bind(this)); - canvas.addEventListener("mouseup", this.handleMouseClick.bind(this)); - canvas.addEventListener("mousedown", this.handleMouseDown.bind(this)); + this.PanZoom.addListener("hover", this.handleMouseMove.bind(this)); + this.PanZoom.addListener("click", this.handleMouseDown.bind(this)); this.on("pallete", this.updatePallete.bind(this)); - // Network.on("canvas", this.handleBatch.bind(this)); Network.waitFor("canvas").then(([pixels]) => this.handleBatch(pixels)); this.draw(); @@ -42,53 +51,24 @@ export class Canvas extends EventEmitter { destroy() { this._destroy = true; - this.canvas.removeEventListener( - "mousemove", - this.handleMouseMove.bind(this) - ); - this.canvas.removeEventListener( - "mouseup", - this.handleMouseClick.bind(this) - ); - this.canvas.removeEventListener( - "mousedown", - this.handleMouseDown.bind(this) - ); + this.PanZoom.removeListener("hover", this.handleMouseMove.bind(this)); + this.PanZoom.removeListener("click", this.handleMouseDown.bind(this)); Network.off("canvas", this.handleBatch.bind(this)); } - private downTime: number | undefined; - private dragOrigin: { x: number; y: number } = { x: 0, y: 0 }; - - handleMouseClick(e: MouseEvent) { - const downDelta = Date.now() - this.downTime!; - const delta = [ - Math.abs(this.dragOrigin.x - e.clientX), - Math.abs(this.dragOrigin.y - e.clientY), - ]; - if (downDelta < 500) { - // mouse was down for less than 500ms - - if (delta[0] < 5 && delta[1] < 5) { - const [x, y] = this.screenToPos(e.clientX, e.clientY); - this.place(x, y); - } - } + handleMouseDown(e: ClickEvent) { + const [x, y] = this.screenToPos(e.clientX, e.clientY); + this.place(x, y); } - handleMouseDown(e: MouseEvent) { - this.downTime = Date.now(); - this.dragOrigin = { x: e.pageX, y: e.pageY }; - } - - handleMouseMove(e: MouseEvent) { + handleMouseMove(e: HoverEvent) { const canvasRect = this.canvas.getBoundingClientRect(); if ( - canvasRect.left <= e.pageX && - canvasRect.right >= e.pageX && - canvasRect.top <= e.pageY && - canvasRect.bottom >= e.pageY + canvasRect.left <= e.clientX && + canvasRect.right >= e.clientX && + canvasRect.top <= e.clientY && + canvasRect.bottom >= e.clientY ) { const [x, y] = this.screenToPos(e.clientX, e.clientY); this.cursor.x = x; @@ -101,8 +81,8 @@ export class Canvas extends EventEmitter { handleBatch(pixels: string[]) { pixels.forEach((hex, index) => { - const x = index / this.config.canvas.size[0]; - const y = index % this.config.canvas.size[0]; + const x = index % this.config.canvas.size[0]; + const y = index / this.config.canvas.size[1]; const color = this.Pallete.getColorFromHex(hex); this.pixels[x + "_" + y] = { @@ -169,14 +149,35 @@ export class Canvas extends EventEmitter { } screenToPos(x: number, y: number) { + // the rendered dimentions in the browser const rect = this.canvas.getBoundingClientRect(); - const scale = [ - this.canvas.width / rect.width, - this.canvas.height / rect.height, - ]; - return [x - rect.left, y - rect.top] - .map((v, i) => v * scale[i]) - .map((v) => v >> 0); + + let output = { + x: 0, + y: 0, + }; + + if (this.PanZoom.flags.useZoom) { + const scale = this.PanZoom.transform.scale; + + output.x = x / scale - rect.left; + output.y = y / scale - rect.top; + } else { + // get the ratio + const scale = [ + this.canvas.width / rect.width, + this.canvas.height / rect.height, + ]; + + output.x = (x - rect.left) * scale[0]; + output.y = (y - rect.top) * scale[1]; + } + + // floor it, we're getting canvas coords, which can't have decimals + output.x >>= 0; + output.y >>= 0; + + return [output.x, output.y]; } draw() { diff --git a/packages/lib/src/renderer/PanZoom.ts b/packages/lib/src/renderer/PanZoom.ts index a0ad976..97d035b 100644 --- a/packages/lib/src/renderer/PanZoom.ts +++ b/packages/lib/src/renderer/PanZoom.ts @@ -11,31 +11,94 @@ import { import { Panning } from "./lib/panning.utils"; interface TransformState { + /** + * Zoom scale + * + * < 0 : zoomed out + * > 0 : zoomed in + */ scale: number; + + /** + * X position of canvas + */ x: number; + + /** + * Y position of canvas + */ y: number; } interface Flags { + /** + * If CSS Zoom is used + * + * CSS Zoom is not supported on Firefox, as it's not a standard + * But on iOS, is fuzzy (ignoring other css rules) when transform: scale()'d up + * + * @see https://caniuse.com/css-zoom + */ useZoom: boolean; } interface TouchState { + /** + * Timestamp of last touch + */ lastTouch: number | null; + + /** + * Distance between each finger when pinch starts + */ pinchStartDistance: number | null; + + /** + * previous distance between each finger + */ lastDistance: number | null; + + /** + * scale when pinch starts + */ pinchStartScale: number | null; + + /** + * middle coord of pinch + */ pinchMidpoint: { x: number; y: number } | null; } -interface MouseState {} +interface MouseState { + /** + * timestamp of mouse down + */ + mouseDown: number | null; +} interface ISetup { + /** + * Scale limits + * [minimum scale, maximum scale] + */ scale: [number, number]; } +// TODO: move these event interfaces out +export interface ClickEvent { + clientX: number; + clientY: number; +} + +export interface HoverEvent { + clientX: number; + clientY: number; +} + interface PanZoomEvents { doubleTap: (e: TouchEvent) => void; + click: (e: ClickEvent) => void; + hover: (e: HoverEvent) => void; } export class PanZoom extends EventEmitter { @@ -68,7 +131,9 @@ export class PanZoom extends EventEmitter { pinchMidpoint: null, }; - this.mouse = {}; + this.mouse = { + mouseDown: null, + }; this.panning = new Panning(this); @@ -318,6 +383,8 @@ export class PanZoom extends EventEmitter { e.preventDefault(); e.stopPropagation(); + this.mouse.mouseDown = Date.now(); + this.panning.start(e.clientX, e.clientY); }, { passive: false } @@ -327,12 +394,18 @@ export class PanZoom extends EventEmitter { document.addEventListener( "mousemove", (e) => { - if (!this.panning.enabled) return; + if (this.panning.enabled) { + e.preventDefault(); + e.stopPropagation(); - e.preventDefault(); - e.stopPropagation(); - - this.panning.move(e.clientX, e.clientY); + this.panning.move(e.clientX, e.clientY); + } else { + // not panning + this.emit("hover", { + clientX: e.clientX, + clientY: e.clientY, + }); + } }, { passive: false } ); @@ -341,12 +414,32 @@ export class PanZoom extends EventEmitter { document.addEventListener( "mouseup", (e) => { - if (!this.panning.enabled) return; + if (this.mouse.mouseDown && Date.now() - this.mouse.mouseDown <= 500) { + // if the mouse was down for less than a half a second, it's a click + // this can't depend on this.panning.enabled because that'll always be true when mouse is down - e.preventDefault(); - e.stopPropagation(); + const delta = [ + Math.abs(this.panning.x - e.clientX), + Math.abs(this.panning.y - e.clientY), + ]; - this.panning.end(e.clientX, e.clientY); + if (delta[0] < 5 && delta[1] < 5) { + // difference from the start position to the up position is very very slow, + // so it's most likely intended to be a click + this.emit("click", { + clientX: e.clientX, + clientY: e.clientY, + }); + } + } + + if (this.panning.enabled) { + // currently panning + e.preventDefault(); + e.stopPropagation(); + + this.panning.end(e.clientX, e.clientY); + } }, { passive: false } );