Loading packages/client/src/components/CanvasWrapper.tsx +130 −0 Original line number Diff line number Diff line Loading @@ -164,6 +164,136 @@ const CanvasInner = () => { captureTemplateArea, } = useCaptureContext(); useEffect(() => { const heldKeys = new Set<string>(); let animationFrame: number | undefined; let previousTime: number | undefined; let velocityX = 0; let velocityY = 0; let viewportChanged = false; const panDirections = new Map<string, readonly [number, number]>(); const addDirection = ( binding: "PAN_UP" | "PAN_DOWN" | "PAN_LEFT" | "PAN_RIGHT", direction: readonly [number, number], ) => { for (const { key } of KeybindManager.getKeybind(binding)) { panDirections.set(key, direction); } }; addDirection("PAN_UP", [0, 1]); addDirection("PAN_DOWN", [0, -1]); addDirection("PAN_LEFT", [1, 0]); addDirection("PAN_RIGHT", [-1, 0]); const snapToDevicePixel = (position: number) => { const screenScale = PanZoom.transform.scale * window.devicePixelRatio; return Math.round(position * screenScale) / screenScale; }; const isTypingTarget = (target: EventTarget | null) => target instanceof HTMLElement && Boolean( target.closest( 'input, textarea, select, [contenteditable="true"], [role="textbox"]', ), ); const moveViewport = (time: number) => { const elapsed = Math.min((time - (previousTime ?? time)) / 1000, 0.05); previousTime = time; const canMove = KeybindManager.isEnabled() && PanZoom.panning.enabled; let horizontal = 0; let vertical = 0; if (canMove) { for (const code of heldKeys) { const [x, y] = panDirections.get(code) ?? [0, 0]; horizontal += x; vertical += y; } } const diagonalScale = horizontal && vertical ? Math.SQRT1_2 : 1; const targetVelocityX = horizontal * 360 * diagonalScale; const targetVelocityY = vertical * 360 * diagonalScale; const easing = 1 - Math.exp(-12 * elapsed); velocityX += (targetVelocityX - velocityX) * easing; velocityY += (targetVelocityY - velocityY) * easing; if (Math.abs(velocityX) > 0.5 || Math.abs(velocityY) > 0.5) { PanZoom.transform.x += (velocityX * elapsed) / PanZoom.transform.scale; PanZoom.transform.y += (velocityY * elapsed) / PanZoom.transform.scale; const visualX = snapToDevicePixel(PanZoom.transform.x); const visualY = snapToDevicePixel(PanZoom.transform.y); PanZoom.$move.style.setProperty( "transform", `translate3d(${visualX}px, ${visualY}px, 0)`, ); viewportChanged = true; animationFrame = requestAnimationFrame(moveViewport); } else if (heldKeys.size && canMove) { animationFrame = requestAnimationFrame(moveViewport); } else { velocityX = 0; velocityY = 0; animationFrame = undefined; previousTime = undefined; if (viewportChanged) { viewportChanged = false; PanZoom.transform.x = snapToDevicePixel(PanZoom.transform.x); PanZoom.transform.y = snapToDevicePixel(PanZoom.transform.y); PanZoom.update(); } } }; const handleKeyDown = (event: KeyboardEvent) => { if ( !KeybindManager.isEnabled() || !panDirections.has(event.code) || event.altKey || event.ctrlKey || event.metaKey || !PanZoom.panning.enabled || isTypingTarget(event.target) ) { return; } heldKeys.add(event.code); if (animationFrame === undefined) { animationFrame = requestAnimationFrame(moveViewport); } event.preventDefault(); }; const handleKeyUp = (event: KeyboardEvent) => { if (heldKeys.delete(event.code)) event.preventDefault(); }; const stopMoving = () => { heldKeys.clear(); velocityX = 0; velocityY = 0; }; document.addEventListener("keydown", handleKeyDown); document.addEventListener("keyup", handleKeyUp); window.addEventListener("blur", stopMoving); return () => { document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("keyup", handleKeyUp); window.removeEventListener("blur", stopMoving); if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); }; }, [PanZoom]); /** * Is the canvas coordinate within the bounds of the canvas? */ Loading packages/client/src/lib/keybinds.ts +8 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,10 @@ interface EmittedKeybind { export const enforceObjectType: EnforceObjectType<IKeybind[]> = (v) => v; const KEYBINDS = enforceObjectType({ PAN_UP: [{ key: "KeyW" }, { key: "ArrowUp" }], PAN_DOWN: [{ key: "KeyS" }, { key: "ArrowDown" }], PAN_LEFT: [{ key: "KeyA" }, { key: "ArrowLeft" }], PAN_RIGHT: [{ key: "KeyD" }, { key: "ArrowRight" }], PIXEL_WHOIS: [ { key: "LCLICK", Loading Loading @@ -303,6 +307,10 @@ class KeybindManager_ extends EventEmitter<{ this.enabled = enabled; } isEnabled() { return this.enabled; } getKeybinds() { return { ...KEYBINDS }; } Loading Loading
packages/client/src/components/CanvasWrapper.tsx +130 −0 Original line number Diff line number Diff line Loading @@ -164,6 +164,136 @@ const CanvasInner = () => { captureTemplateArea, } = useCaptureContext(); useEffect(() => { const heldKeys = new Set<string>(); let animationFrame: number | undefined; let previousTime: number | undefined; let velocityX = 0; let velocityY = 0; let viewportChanged = false; const panDirections = new Map<string, readonly [number, number]>(); const addDirection = ( binding: "PAN_UP" | "PAN_DOWN" | "PAN_LEFT" | "PAN_RIGHT", direction: readonly [number, number], ) => { for (const { key } of KeybindManager.getKeybind(binding)) { panDirections.set(key, direction); } }; addDirection("PAN_UP", [0, 1]); addDirection("PAN_DOWN", [0, -1]); addDirection("PAN_LEFT", [1, 0]); addDirection("PAN_RIGHT", [-1, 0]); const snapToDevicePixel = (position: number) => { const screenScale = PanZoom.transform.scale * window.devicePixelRatio; return Math.round(position * screenScale) / screenScale; }; const isTypingTarget = (target: EventTarget | null) => target instanceof HTMLElement && Boolean( target.closest( 'input, textarea, select, [contenteditable="true"], [role="textbox"]', ), ); const moveViewport = (time: number) => { const elapsed = Math.min((time - (previousTime ?? time)) / 1000, 0.05); previousTime = time; const canMove = KeybindManager.isEnabled() && PanZoom.panning.enabled; let horizontal = 0; let vertical = 0; if (canMove) { for (const code of heldKeys) { const [x, y] = panDirections.get(code) ?? [0, 0]; horizontal += x; vertical += y; } } const diagonalScale = horizontal && vertical ? Math.SQRT1_2 : 1; const targetVelocityX = horizontal * 360 * diagonalScale; const targetVelocityY = vertical * 360 * diagonalScale; const easing = 1 - Math.exp(-12 * elapsed); velocityX += (targetVelocityX - velocityX) * easing; velocityY += (targetVelocityY - velocityY) * easing; if (Math.abs(velocityX) > 0.5 || Math.abs(velocityY) > 0.5) { PanZoom.transform.x += (velocityX * elapsed) / PanZoom.transform.scale; PanZoom.transform.y += (velocityY * elapsed) / PanZoom.transform.scale; const visualX = snapToDevicePixel(PanZoom.transform.x); const visualY = snapToDevicePixel(PanZoom.transform.y); PanZoom.$move.style.setProperty( "transform", `translate3d(${visualX}px, ${visualY}px, 0)`, ); viewportChanged = true; animationFrame = requestAnimationFrame(moveViewport); } else if (heldKeys.size && canMove) { animationFrame = requestAnimationFrame(moveViewport); } else { velocityX = 0; velocityY = 0; animationFrame = undefined; previousTime = undefined; if (viewportChanged) { viewportChanged = false; PanZoom.transform.x = snapToDevicePixel(PanZoom.transform.x); PanZoom.transform.y = snapToDevicePixel(PanZoom.transform.y); PanZoom.update(); } } }; const handleKeyDown = (event: KeyboardEvent) => { if ( !KeybindManager.isEnabled() || !panDirections.has(event.code) || event.altKey || event.ctrlKey || event.metaKey || !PanZoom.panning.enabled || isTypingTarget(event.target) ) { return; } heldKeys.add(event.code); if (animationFrame === undefined) { animationFrame = requestAnimationFrame(moveViewport); } event.preventDefault(); }; const handleKeyUp = (event: KeyboardEvent) => { if (heldKeys.delete(event.code)) event.preventDefault(); }; const stopMoving = () => { heldKeys.clear(); velocityX = 0; velocityY = 0; }; document.addEventListener("keydown", handleKeyDown); document.addEventListener("keyup", handleKeyUp); window.addEventListener("blur", stopMoving); return () => { document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("keyup", handleKeyUp); window.removeEventListener("blur", stopMoving); if (animationFrame !== undefined) cancelAnimationFrame(animationFrame); }; }, [PanZoom]); /** * Is the canvas coordinate within the bounds of the canvas? */ Loading
packages/client/src/lib/keybinds.ts +8 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,10 @@ interface EmittedKeybind { export const enforceObjectType: EnforceObjectType<IKeybind[]> = (v) => v; const KEYBINDS = enforceObjectType({ PAN_UP: [{ key: "KeyW" }, { key: "ArrowUp" }], PAN_DOWN: [{ key: "KeyS" }, { key: "ArrowDown" }], PAN_LEFT: [{ key: "KeyA" }, { key: "ArrowLeft" }], PAN_RIGHT: [{ key: "KeyD" }, { key: "ArrowRight" }], PIXEL_WHOIS: [ { key: "LCLICK", Loading Loading @@ -303,6 +307,10 @@ class KeybindManager_ extends EventEmitter<{ this.enabled = enabled; } isEnabled() { return this.enabled; } getKeybinds() { return { ...KEYBINDS }; } Loading