Loading packages/client/src/components/CanvasWrapper.tsx +12 −5 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ const KEYBOARD_ZOOM_STEP = 1; const KEYBOARD_ZOOM_SMALL_STEP = 0.35; const INSPECT_END_ANIMATION_DURATION = 180; const TOUCH_INSPECT_MOVE_THRESHOLD = 25; const GRID_LINE_FRACTION = 1 / 8; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); Loading Loading @@ -122,7 +123,7 @@ export const CanvasWrapper = () => { } const CursorInspectPreview = () => { const { cursor } = useAppContext(); const { cursor, gridOverlay } = useAppContext(); const PanZoom = useContext(RendererContext); const selectedColor = typeof cursor.color === "number" Loading Loading @@ -342,6 +343,8 @@ const CursorInspectPreview = () => { const inspectScreenRect = (() => { if ( (inspectHold === "idle" && !hasSelectedColor) || typeof previewPosition.x !== "number" || typeof previewPosition.y !== "number" || previewPosition.x < 0 || previewPosition.y < 0 ) { Loading @@ -362,6 +365,10 @@ const CursorInspectPreview = () => { const inspectBorderSize = inspectScreenRect ? Math.min(inspectScreenRect.width, inspectScreenRect.height) : 1; const gridInset = inspectScreenRect && gridOverlay.enabled && hasSelectedColor ? inspectBorderSize * GRID_LINE_FRACTION : 0; const preview = ( <div Loading @@ -382,10 +389,10 @@ const CursorInspectPreview = () => { left: previewPosition.x, ...(inspectScreenRect && { position: "fixed", top: inspectScreenRect.top, left: inspectScreenRect.left, width: inspectScreenRect.width, height: inspectScreenRect.height, top: inspectScreenRect.top + gridInset, left: inspectScreenRect.left + gridInset, width: Math.max(0, inspectScreenRect.width - gridInset), height: Math.max(0, inspectScreenRect.height - gridInset), zIndex: 1, "--inspect-border-start-inner": inspectBorderSize * 0.02 + "px", "--inspect-border-start-outer": inspectBorderSize * 0.04 + "px", Loading packages/client/src/components/Overlay/GridOverlay.tsx +7 −3 Original line number Diff line number Diff line Loading @@ -28,7 +28,10 @@ export const GridOverlay = () => { if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; // full black // Keep grid lines neutral instead of tinting them with placed pixel colors. const opacity = Math.min(Math.max(gridOverlay.opacity ?? 1, 0), 1); const channel = Math.round(255 - (255 - 128) * opacity); ctx.fillStyle = `rgb(${channel}, ${channel}, ${channel})`; for (let x = 0; x <= canvas.width; x += GRID_SPACING) { ctx.fillRect(x, 0, 1, canvas.height); // vertical line as 1px wide column Loading @@ -37,7 +40,7 @@ export const GridOverlay = () => { for (let y = 0; y <= canvas.height; y += GRID_SPACING) { ctx.fillRect(0, y, canvas.width, 1); // horizontal line as 1px tall row } }, [gridOverlay.enabled]); }, [gridOverlay.enabled, gridOverlay.opacity]); return ( <canvas Loading @@ -53,8 +56,9 @@ export const GridOverlay = () => { width: config.canvas.size[0], height: config.canvas.size[1], display: gridOverlay.enabled ? "block" : "none", opacity: gridOverlay.opacity?.toFixed(1) ?? "1", opacity: 1, pointerEvents: "none", zIndex: 1, }} /> ); Loading packages/client/src/lib/canvas.ts +2 −2 Original line number Diff line number Diff line Loading @@ -881,8 +881,8 @@ export class CanvasCore extends EventEmitter<CanvasEvents> { canvasY += this.canvas.height; } canvasX >>= 0; canvasY >>= 0; canvasX = Math.round(canvasX); canvasY = Math.round(canvasY); return { canvasX, canvasY, zoom }; } Loading packages/client/src/lib/router.ts +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ class _Router extends EventEmitter<RouterEvents> { params.set(CLIENT_PARAMS.canvas_y, position.canvasY + ""); params.set( CLIENT_PARAMS.canvas_zoom, (this.PanZoom!.transform.scale >> 0) + "", Math.round(this.PanZoom!.transform.scale) + "", ); if (this.templateState.enabled && this.templateState.url) { Loading packages/lib/src/renderer/PanZoom.ts +50 −2 Original line number Diff line number Diff line Loading @@ -152,6 +152,8 @@ interface PanZoomEvents { export class PanZoom extends EventEmitter<PanZoomEvents> { private initialized = false; private resizeFrame: number | undefined; private devicePixelOrigin = { x: 0, y: 0 }; // eslint-disable-next-line @typescript-eslint/no-explicit-any public $wrapper: HTMLDivElement = null as any; Loading Loading @@ -221,6 +223,7 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.registerMouseEvents(); this.registerTouchEvents(); window.addEventListener("resize", this.handleResize); this.initialized = true; Loading @@ -233,12 +236,24 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.transform.x = x; this.transform.y = y; this.transform.scale = scale; this.update({ suppressEmit: true }); } this.update({ suppressEmit: true }); this.emit("initialize"); } private handleResize = () => { if (this.resizeFrame !== undefined) { cancelAnimationFrame(this.resizeFrame); } this.resizeFrame = requestAnimationFrame(() => { this.resizeFrame = undefined; if (this.initialized) this.update({ suppressEmit: true }); }); }; /** * Get scale that would fit the zoom element in the viewport * @returns Loading Loading @@ -859,9 +874,17 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { ); } const zoomRect = this.$zoom.getBoundingClientRect(); this.devicePixelOrigin.x = zoomRect.left; this.devicePixelOrigin.y = zoomRect.top; const position = this.alignToDevicePixel( this.transform.x, this.transform.y, ); this.$move.style.setProperty( "transform", `translate(${this.transform.x}px, ${this.transform.y}px)`, `translate(${position.x}px, ${position.y}px)`, ); if (!suppressEmit) { Loading @@ -879,10 +902,35 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.endInspectHold(); this.unregisterTouchEvents(); this.unregisterMouseEvents(); window.removeEventListener("resize", this.handleResize); if (this.resizeFrame !== undefined) { cancelAnimationFrame(this.resizeFrame); this.resizeFrame = undefined; } } // utilities alignToDevicePixel(x: number, y: number) { const scale = this.transform.scale; const dpr = window.devicePixelRatio; const screenScale = scale * dpr; if (!Number.isFinite(screenScale) || screenScale <= 0) return { x, y }; return { x: (Math.round((this.devicePixelOrigin.x + x * scale) * dpr) / dpr - this.devicePixelOrigin.x) / scale, y: (Math.round((this.devicePixelOrigin.y + y * scale) * dpr) / dpr - this.devicePixelOrigin.y) / scale, }; } nudgeScale(adj: number) { this.transform.scale = checkZoomBounds( this.transform.scale * 1.5 ** adj, Loading Loading
packages/client/src/components/CanvasWrapper.tsx +12 −5 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ const KEYBOARD_ZOOM_STEP = 1; const KEYBOARD_ZOOM_SMALL_STEP = 0.35; const INSPECT_END_ANIMATION_DURATION = 180; const TOUCH_INSPECT_MOVE_THRESHOLD = 25; const GRID_LINE_FRACTION = 1 / 8; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); Loading Loading @@ -122,7 +123,7 @@ export const CanvasWrapper = () => { } const CursorInspectPreview = () => { const { cursor } = useAppContext(); const { cursor, gridOverlay } = useAppContext(); const PanZoom = useContext(RendererContext); const selectedColor = typeof cursor.color === "number" Loading Loading @@ -342,6 +343,8 @@ const CursorInspectPreview = () => { const inspectScreenRect = (() => { if ( (inspectHold === "idle" && !hasSelectedColor) || typeof previewPosition.x !== "number" || typeof previewPosition.y !== "number" || previewPosition.x < 0 || previewPosition.y < 0 ) { Loading @@ -362,6 +365,10 @@ const CursorInspectPreview = () => { const inspectBorderSize = inspectScreenRect ? Math.min(inspectScreenRect.width, inspectScreenRect.height) : 1; const gridInset = inspectScreenRect && gridOverlay.enabled && hasSelectedColor ? inspectBorderSize * GRID_LINE_FRACTION : 0; const preview = ( <div Loading @@ -382,10 +389,10 @@ const CursorInspectPreview = () => { left: previewPosition.x, ...(inspectScreenRect && { position: "fixed", top: inspectScreenRect.top, left: inspectScreenRect.left, width: inspectScreenRect.width, height: inspectScreenRect.height, top: inspectScreenRect.top + gridInset, left: inspectScreenRect.left + gridInset, width: Math.max(0, inspectScreenRect.width - gridInset), height: Math.max(0, inspectScreenRect.height - gridInset), zIndex: 1, "--inspect-border-start-inner": inspectBorderSize * 0.02 + "px", "--inspect-border-start-outer": inspectBorderSize * 0.04 + "px", Loading
packages/client/src/components/Overlay/GridOverlay.tsx +7 −3 Original line number Diff line number Diff line Loading @@ -28,7 +28,10 @@ export const GridOverlay = () => { if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; // full black // Keep grid lines neutral instead of tinting them with placed pixel colors. const opacity = Math.min(Math.max(gridOverlay.opacity ?? 1, 0), 1); const channel = Math.round(255 - (255 - 128) * opacity); ctx.fillStyle = `rgb(${channel}, ${channel}, ${channel})`; for (let x = 0; x <= canvas.width; x += GRID_SPACING) { ctx.fillRect(x, 0, 1, canvas.height); // vertical line as 1px wide column Loading @@ -37,7 +40,7 @@ export const GridOverlay = () => { for (let y = 0; y <= canvas.height; y += GRID_SPACING) { ctx.fillRect(0, y, canvas.width, 1); // horizontal line as 1px tall row } }, [gridOverlay.enabled]); }, [gridOverlay.enabled, gridOverlay.opacity]); return ( <canvas Loading @@ -53,8 +56,9 @@ export const GridOverlay = () => { width: config.canvas.size[0], height: config.canvas.size[1], display: gridOverlay.enabled ? "block" : "none", opacity: gridOverlay.opacity?.toFixed(1) ?? "1", opacity: 1, pointerEvents: "none", zIndex: 1, }} /> ); Loading
packages/client/src/lib/canvas.ts +2 −2 Original line number Diff line number Diff line Loading @@ -881,8 +881,8 @@ export class CanvasCore extends EventEmitter<CanvasEvents> { canvasY += this.canvas.height; } canvasX >>= 0; canvasY >>= 0; canvasX = Math.round(canvasX); canvasY = Math.round(canvasY); return { canvasX, canvasY, zoom }; } Loading
packages/client/src/lib/router.ts +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ class _Router extends EventEmitter<RouterEvents> { params.set(CLIENT_PARAMS.canvas_y, position.canvasY + ""); params.set( CLIENT_PARAMS.canvas_zoom, (this.PanZoom!.transform.scale >> 0) + "", Math.round(this.PanZoom!.transform.scale) + "", ); if (this.templateState.enabled && this.templateState.url) { Loading
packages/lib/src/renderer/PanZoom.ts +50 −2 Original line number Diff line number Diff line Loading @@ -152,6 +152,8 @@ interface PanZoomEvents { export class PanZoom extends EventEmitter<PanZoomEvents> { private initialized = false; private resizeFrame: number | undefined; private devicePixelOrigin = { x: 0, y: 0 }; // eslint-disable-next-line @typescript-eslint/no-explicit-any public $wrapper: HTMLDivElement = null as any; Loading Loading @@ -221,6 +223,7 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.registerMouseEvents(); this.registerTouchEvents(); window.addEventListener("resize", this.handleResize); this.initialized = true; Loading @@ -233,12 +236,24 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.transform.x = x; this.transform.y = y; this.transform.scale = scale; this.update({ suppressEmit: true }); } this.update({ suppressEmit: true }); this.emit("initialize"); } private handleResize = () => { if (this.resizeFrame !== undefined) { cancelAnimationFrame(this.resizeFrame); } this.resizeFrame = requestAnimationFrame(() => { this.resizeFrame = undefined; if (this.initialized) this.update({ suppressEmit: true }); }); }; /** * Get scale that would fit the zoom element in the viewport * @returns Loading Loading @@ -859,9 +874,17 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { ); } const zoomRect = this.$zoom.getBoundingClientRect(); this.devicePixelOrigin.x = zoomRect.left; this.devicePixelOrigin.y = zoomRect.top; const position = this.alignToDevicePixel( this.transform.x, this.transform.y, ); this.$move.style.setProperty( "transform", `translate(${this.transform.x}px, ${this.transform.y}px)`, `translate(${position.x}px, ${position.y}px)`, ); if (!suppressEmit) { Loading @@ -879,10 +902,35 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.endInspectHold(); this.unregisterTouchEvents(); this.unregisterMouseEvents(); window.removeEventListener("resize", this.handleResize); if (this.resizeFrame !== undefined) { cancelAnimationFrame(this.resizeFrame); this.resizeFrame = undefined; } } // utilities alignToDevicePixel(x: number, y: number) { const scale = this.transform.scale; const dpr = window.devicePixelRatio; const screenScale = scale * dpr; if (!Number.isFinite(screenScale) || screenScale <= 0) return { x, y }; return { x: (Math.round((this.devicePixelOrigin.x + x * scale) * dpr) / dpr - this.devicePixelOrigin.x) / scale, y: (Math.round((this.devicePixelOrigin.y + y * scale) * dpr) / dpr - this.devicePixelOrigin.y) / scale, }; } nudgeScale(adj: number) { this.transform.scale = checkZoomBounds( this.transform.scale * 1.5 ** adj, Loading