Loading packages/client/src/components/CanvasWrapper.tsx +57 −6 Original line number Diff line number Diff line Loading @@ -19,17 +19,65 @@ export const CanvasWrapper = () => { ); }; const parseHashParams = (canvas: Canvas) => { // maybe move this to a utility inside routes.ts let { hash } = new URL(window.location.href); if (hash.indexOf("#") === 0) { hash = hash.slice(1); } let params = new URLSearchParams(hash); let position: { x?: number; y?: number; zoom?: number; } = {}; if (params.has("x") && !isNaN(parseInt(params.get("x")!))) position.x = parseInt(params.get("x")!); if (params.has("y") && !isNaN(parseInt(params.get("y")!))) position.y = parseInt(params.get("y")!); if (params.has("zoom") && !isNaN(parseInt(params.get("zoom")!))) position.zoom = parseInt(params.get("zoom")!); if ( typeof position.x === "number" && typeof position.y === "number" && typeof position.zoom === "number" ) { const { transformX, transformY } = canvas.canvasToPanZoomTransform( position.x, position.y ); return { x: transformX, y: transformY, zoom: position.zoom, }; } }; const CanvasInner = () => { const canvasRef = createRef<HTMLCanvasElement>(); const { config, setCanvasPosition, setCursorPosition } = useAppContext(); const PanZoom = useContext(RendererContext); // const { centerView } = useControls(); useEffect(() => { if (!config.canvas || !canvasRef.current) return; const canvas = canvasRef.current!; const canvasInstance = new Canvas(config, canvas, PanZoom); // centerView(); { // TODO: handle hash changes and move viewport // NOTE: this will need to be cancelled if handleViewportMove was executed recently const position = parseHashParams(canvasInstance); if (position) { PanZoom.setPosition(position, { suppressEmit: true }); } } const handleViewportMove = throttle((state: ViewportMoveEvent) => { const pos = canvasInstance.panZoomTransformToCanvas(); Loading @@ -45,7 +93,7 @@ const CanvasInner = () => { window.location.replace(Routes.canvas(canvasPosition)); }, 1000); const handleCursorPos = (pos: IPosition) => { const handleCursorPos = throttle((pos: IPosition) => { if ( pos.x < 0 || pos.y < 0 || Loading @@ -54,9 +102,10 @@ const CanvasInner = () => { ) { setCursorPosition(); } else { setCursorPosition(pos); // fixes not passing the current value setCursorPosition({ ...pos }); } }; }, 1); PanZoom.addListener("viewportMove", handleViewportMove); canvasInstance.on("cursorPos", handleCursorPos); Loading @@ -66,7 +115,9 @@ const CanvasInner = () => { PanZoom.removeListener("viewportMove", handleViewportMove); canvasInstance.off("cursorPos", handleCursorPos); }; }, [PanZoom, canvasRef, config, setCanvasPosition]); // do not include canvasRef, it causes infinite re-renders }, [PanZoom, config, setCanvasPosition, setCursorPosition]); return ( <canvas Loading packages/client/src/lib/canvas.ts +16 −0 Original line number Diff line number Diff line Loading @@ -158,6 +158,22 @@ export class Canvas extends EventEmitter<CanvasEvents> { }); } canvasToPanZoomTransform(x: number, y: number) { let transformX = 0; let transformY = 0; if (this.PanZoom.flags.useZoom) { // CSS Zoom does not alter this (obviously) transformX = this.canvas.width / 2 - x; transformY = this.canvas.height / 2 - y; } else { transformX = this.canvas.width / 2 - x; transformY = this.canvas.height / 2 - y; } return { transformX, transformY }; } panZoomTransformToCanvas() { const { x, y, scale: zoom } = this.PanZoom.transform; const rect = this.canvas.getBoundingClientRect(); Loading packages/lib/src/renderer/PanZoom.ts +74 −6 Original line number Diff line number Diff line Loading @@ -82,6 +82,8 @@ interface ISetup { * [minimum scale, maximum scale] */ scale: [number, number]; initialTransform?: TransformState; } // TODO: move these event interfaces out Loading @@ -106,9 +108,12 @@ interface PanZoomEvents { click: (e: ClickEvent) => void; hover: (e: HoverEvent) => void; viewportMove: (e: ViewportMoveEvent) => void; initialize: () => void; } export class PanZoom extends EventEmitter<PanZoomEvents> { private initialized = false; public $wrapper: HTMLDivElement = null as any; public $zoom: HTMLDivElement = null as any; public $move: HTMLDivElement = null as any; Loading Loading @@ -165,6 +170,53 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.detectFlags(); this.registerMouseEvents(); this.registerTouchEvents(); this.initialized = true; if (this.setup.initialTransform) { // use initial transform if it is set // initialTransform is set from #setPosition() when PanZoom is not initalized let { x, y, scale } = this.setup.initialTransform; this.transform.x = x; this.transform.y = y; this.transform.scale = scale; this.update({ suppressEmit: true }); } this.emit("initialize"); } /** * Sets transform data * * @param position * @param position.x Transform X * @param position.y Transform Y * @param position.zoom Zoom scale * @param flags * @param flags.suppressEmit If true, don't emit a viewport change * @returns */ setPosition( { x, y, zoom }: { x: number; y: number; zoom: number }, { suppressEmit } = { suppressEmit: false } ) { if (!this.initialized) { // elements are not yet available, store them to be used upon initialization this.setup.initialTransform = { x, y, scale: zoom, }; return; } this.transform.x = x; this.transform.y = y; this.transform.scale = zoom; this.update({ suppressEmit }); } detectFlags() { Loading Loading @@ -453,12 +505,28 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { ); } update() { /** * Update viewport scale and position * * @param flags * @param flags.suppressEmit Do not emit viewportMove */ update( { suppressEmit, }: { suppressEmit: boolean; } = { suppressEmit: false, } ) { if (!suppressEmit) { this.emit("viewportMove", { scale: this.transform.scale, x: this.transform.x, y: this.transform.y, }); } if (this.flags.useZoom) { this.$zoom.style.setProperty("zoom", this.transform.scale * 100 + "%"); Loading Loading
packages/client/src/components/CanvasWrapper.tsx +57 −6 Original line number Diff line number Diff line Loading @@ -19,17 +19,65 @@ export const CanvasWrapper = () => { ); }; const parseHashParams = (canvas: Canvas) => { // maybe move this to a utility inside routes.ts let { hash } = new URL(window.location.href); if (hash.indexOf("#") === 0) { hash = hash.slice(1); } let params = new URLSearchParams(hash); let position: { x?: number; y?: number; zoom?: number; } = {}; if (params.has("x") && !isNaN(parseInt(params.get("x")!))) position.x = parseInt(params.get("x")!); if (params.has("y") && !isNaN(parseInt(params.get("y")!))) position.y = parseInt(params.get("y")!); if (params.has("zoom") && !isNaN(parseInt(params.get("zoom")!))) position.zoom = parseInt(params.get("zoom")!); if ( typeof position.x === "number" && typeof position.y === "number" && typeof position.zoom === "number" ) { const { transformX, transformY } = canvas.canvasToPanZoomTransform( position.x, position.y ); return { x: transformX, y: transformY, zoom: position.zoom, }; } }; const CanvasInner = () => { const canvasRef = createRef<HTMLCanvasElement>(); const { config, setCanvasPosition, setCursorPosition } = useAppContext(); const PanZoom = useContext(RendererContext); // const { centerView } = useControls(); useEffect(() => { if (!config.canvas || !canvasRef.current) return; const canvas = canvasRef.current!; const canvasInstance = new Canvas(config, canvas, PanZoom); // centerView(); { // TODO: handle hash changes and move viewport // NOTE: this will need to be cancelled if handleViewportMove was executed recently const position = parseHashParams(canvasInstance); if (position) { PanZoom.setPosition(position, { suppressEmit: true }); } } const handleViewportMove = throttle((state: ViewportMoveEvent) => { const pos = canvasInstance.panZoomTransformToCanvas(); Loading @@ -45,7 +93,7 @@ const CanvasInner = () => { window.location.replace(Routes.canvas(canvasPosition)); }, 1000); const handleCursorPos = (pos: IPosition) => { const handleCursorPos = throttle((pos: IPosition) => { if ( pos.x < 0 || pos.y < 0 || Loading @@ -54,9 +102,10 @@ const CanvasInner = () => { ) { setCursorPosition(); } else { setCursorPosition(pos); // fixes not passing the current value setCursorPosition({ ...pos }); } }; }, 1); PanZoom.addListener("viewportMove", handleViewportMove); canvasInstance.on("cursorPos", handleCursorPos); Loading @@ -66,7 +115,9 @@ const CanvasInner = () => { PanZoom.removeListener("viewportMove", handleViewportMove); canvasInstance.off("cursorPos", handleCursorPos); }; }, [PanZoom, canvasRef, config, setCanvasPosition]); // do not include canvasRef, it causes infinite re-renders }, [PanZoom, config, setCanvasPosition, setCursorPosition]); return ( <canvas Loading
packages/client/src/lib/canvas.ts +16 −0 Original line number Diff line number Diff line Loading @@ -158,6 +158,22 @@ export class Canvas extends EventEmitter<CanvasEvents> { }); } canvasToPanZoomTransform(x: number, y: number) { let transformX = 0; let transformY = 0; if (this.PanZoom.flags.useZoom) { // CSS Zoom does not alter this (obviously) transformX = this.canvas.width / 2 - x; transformY = this.canvas.height / 2 - y; } else { transformX = this.canvas.width / 2 - x; transformY = this.canvas.height / 2 - y; } return { transformX, transformY }; } panZoomTransformToCanvas() { const { x, y, scale: zoom } = this.PanZoom.transform; const rect = this.canvas.getBoundingClientRect(); Loading
packages/lib/src/renderer/PanZoom.ts +74 −6 Original line number Diff line number Diff line Loading @@ -82,6 +82,8 @@ interface ISetup { * [minimum scale, maximum scale] */ scale: [number, number]; initialTransform?: TransformState; } // TODO: move these event interfaces out Loading @@ -106,9 +108,12 @@ interface PanZoomEvents { click: (e: ClickEvent) => void; hover: (e: HoverEvent) => void; viewportMove: (e: ViewportMoveEvent) => void; initialize: () => void; } export class PanZoom extends EventEmitter<PanZoomEvents> { private initialized = false; public $wrapper: HTMLDivElement = null as any; public $zoom: HTMLDivElement = null as any; public $move: HTMLDivElement = null as any; Loading Loading @@ -165,6 +170,53 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { this.detectFlags(); this.registerMouseEvents(); this.registerTouchEvents(); this.initialized = true; if (this.setup.initialTransform) { // use initial transform if it is set // initialTransform is set from #setPosition() when PanZoom is not initalized let { x, y, scale } = this.setup.initialTransform; this.transform.x = x; this.transform.y = y; this.transform.scale = scale; this.update({ suppressEmit: true }); } this.emit("initialize"); } /** * Sets transform data * * @param position * @param position.x Transform X * @param position.y Transform Y * @param position.zoom Zoom scale * @param flags * @param flags.suppressEmit If true, don't emit a viewport change * @returns */ setPosition( { x, y, zoom }: { x: number; y: number; zoom: number }, { suppressEmit } = { suppressEmit: false } ) { if (!this.initialized) { // elements are not yet available, store them to be used upon initialization this.setup.initialTransform = { x, y, scale: zoom, }; return; } this.transform.x = x; this.transform.y = y; this.transform.scale = zoom; this.update({ suppressEmit }); } detectFlags() { Loading Loading @@ -453,12 +505,28 @@ export class PanZoom extends EventEmitter<PanZoomEvents> { ); } update() { /** * Update viewport scale and position * * @param flags * @param flags.suppressEmit Do not emit viewportMove */ update( { suppressEmit, }: { suppressEmit: boolean; } = { suppressEmit: false, } ) { if (!suppressEmit) { this.emit("viewportMove", { scale: this.transform.scale, x: this.transform.x, y: this.transform.y, }); } if (this.flags.useZoom) { this.$zoom.style.setProperty("zoom", this.transform.scale * 100 + "%"); Loading