Loading packages/client/src/components/CanvasWrapper.tsx +46 −63 Original line number Diff line number Diff line Loading @@ -5,9 +5,9 @@ import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer"; import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext"; import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom"; import throttle from "lodash.throttle"; import { Routes } from "../lib/routes"; import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net"; import { IPosition } from "@sc07-canvas/lib/src/net"; import { Template } from "./Template"; import { IRouterData, Router } from "../lib/router"; export const CanvasWrapper = () => { // to prevent safari from blurring things, use the zoom css property Loading @@ -21,79 +21,60 @@ 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); useEffect(() => { Router.PanZoom = PanZoom; // @ts-ignore window.TEST_Router = Router; }, [PanZoom]); useEffect(() => { if (!config.canvas || !canvasRef.current) return; const canvas = canvasRef.current!; const canvasInstance = new Canvas(config, canvas, PanZoom); const initAt = Date.now(); { // TODO: handle hash changes and move viewport // NOTE: this will need to be cancelled if handleViewportMove was executed recently const handleNavigate = (data: IRouterData) => { if (data.canvas) { const position = canvasInstance.canvasToPanZoomTransform( data.canvas.x, data.canvas.y ); const position = parseHashParams(canvasInstance); if (position) { PanZoom.setPosition(position, { suppressEmit: true }); } PanZoom.setPosition( { x: position.transformX, y: position.transformY, zoom: data.canvas.zoom || 0, // TODO: fit canvas to viewport instead of defaulting }, { suppressEmit: true } ); } const handleViewportMove = throttle((state: ViewportMoveEvent) => { const pos = canvasInstance.panZoomTransformToCanvas(); const canvasPosition: ICanvasPosition = { x: pos.canvasX, y: pos.canvasY, zoom: state.scale >> 0, }; setCanvasPosition(canvasPosition); // initial load const initialRouter = Router.get(); console.log( "[CanvasWrapper] Initial router data, handling navigate", initialRouter ); handleNavigate(initialRouter); window.location.replace(Routes.canvas({ pos: canvasPosition })); }, 1000); const handleViewportMove = (state: ViewportMoveEvent) => { if (Date.now() - initAt < 60 * 1000) { console.debug( "[CanvasWrapper] handleViewportMove called soon after init", Date.now() - initAt ); } Router.queueUpdate(); }; const handleCursorPos = throttle((pos: IPosition) => { if ( Loading @@ -111,11 +92,13 @@ const CanvasInner = () => { PanZoom.addListener("viewportMove", handleViewportMove); canvasInstance.on("cursorPos", handleCursorPos); Router.on("navigate", handleNavigate); return () => { canvasInstance.destroy(); PanZoom.removeListener("viewportMove", handleViewportMove); canvasInstance.off("cursorPos", handleCursorPos); Router.off("navigate", handleNavigate); }; // ! do not include canvasRef, it causes infinite re-renders Loading packages/client/src/contexts/TemplateContext.tsx +41 −6 Original line number Diff line number Diff line import { PropsWithChildren, createContext, useContext, useState } from "react"; import { PropsWithChildren, createContext, useContext, useEffect, useState, } from "react"; import { IRouterData, Router } from "../lib/router"; interface ITemplate { /** Loading Loading @@ -35,13 +42,41 @@ const templateContext = createContext<ITemplate>({} as any); export const useTemplateContext = () => useContext(templateContext); export const TemplateContext = ({ children }: PropsWithChildren) => { const [enable, setEnable] = useState(false); const [url, setURL] = useState<string>(); const [width, setWidth] = useState<number>(); const [x, setX] = useState(0); const [y, setY] = useState(0); const routerData = Router.get(); const [enable, setEnable] = useState(!!routerData.template?.url); const [url, setURL] = useState<string | undefined>(routerData.template?.url); const [width, setWidth] = useState<number | undefined>( routerData.template?.width ); const [x, setX] = useState(routerData.template?.x || 0); const [y, setY] = useState(routerData.template?.y || 0); const [opacity, setOpacity] = useState(100); useEffect(() => { const handleNavigate = (data: IRouterData) => { if (data.template) { setEnable(true); setURL(data.template.url); setWidth(data.template.width); setX(data.template.x || 0); setY(data.template.y || 0); } else { setEnable(false); } }; Router.on("navigate", handleNavigate); return () => { Router.off("navigate", handleNavigate); }; }, []); useEffect(() => { Router.setTemplate({ enabled: enable, width, x, y, url }); Router.queueUpdate(); }, [enable, width, x, y, url]); return ( <templateContext.Provider value={{ Loading packages/client/src/lib/router.ts 0 → 100644 +216 −0 Original line number Diff line number Diff line import { PanZoom } from "@sc07-canvas/lib/src/renderer/PanZoom"; import { Canvas } from "./canvas"; import throttle from "lodash.throttle"; import EventEmitter from "eventemitter3"; const CLIENT_PARAMS = { canvas_x: "x", canvas_y: "y", canvas_zoom: "zoom", template_url: "tu", template_width: "tw", template_x: "tx", template_y: "ty", }; export interface IRouterData { canvas?: { x: number; y: number; zoom?: number; }; template?: { url: string; width?: number; x?: number; y?: number; }; } interface RouterEvents { navigate(route: IRouterData): void; } class _Router extends EventEmitter<RouterEvents> { PanZoom: PanZoom | undefined; // React TemplateContext templateState: { enabled: boolean; width?: number; x: number; y: number; url?: string; } = { enabled: false, x: 0, y: 0, }; constructor() { super(); window.addEventListener("hashchange", this._hashChange.bind(this)); } destroy() { // NOTE: this method *never* gets called because this is intended to be global window.removeEventListener("hashchange", this._hashChange.bind(this)); } _hashChange(e: HashChangeEvent) { const data = this.get(); console.info("[Router] Navigated", data); this.emit("navigate", data); } queueUpdate = throttle(this.update, 500); update() { const url = this.getURL(); if (!url) return; console.log("[Router] Updating URL"); window.history.replaceState({}, "", url); } getURL() { const canvas = Canvas.instance; // this is not that helpful because the data is more spread out // this gets replaced by using TemplateContext data // const template = Template.instance; if (!canvas) { console.warn("Router#update called but no canvas instance exists"); return; } if (!this.PanZoom) { console.warn("Router#update called but no PanZoom instance exists"); } const params = new URLSearchParams(); const position = canvas.panZoomTransformToCanvas(); params.set(CLIENT_PARAMS.canvas_x, position.canvasX + ""); params.set(CLIENT_PARAMS.canvas_y, position.canvasY + ""); params.set( CLIENT_PARAMS.canvas_zoom, (this.PanZoom!.transform.scale >> 0) + "" ); if (this.templateState.enabled && this.templateState.url) { params.set(CLIENT_PARAMS.template_url, this.templateState.url + ""); if (this.templateState.width) params.set(CLIENT_PARAMS.template_width, this.templateState.width + ""); params.set(CLIENT_PARAMS.template_x, this.templateState.x + ""); params.set(CLIENT_PARAMS.template_y, this.templateState.y + ""); } return ( window.location.protocol + "//" + window.location.host + "/#" + params ); } /** * Parse the URL and return what was found, following specifications * There's no defaults, if it's not specified in the url, it's not specified in the return * * @returns */ get(): IRouterData { const params = new URLSearchParams(window.location.hash.slice(1)); let canvas: | { x: number; y: number; zoom?: number; } | undefined = undefined; if ( params.has(CLIENT_PARAMS.canvas_x) && params.has(CLIENT_PARAMS.canvas_y) ) { // params exist, now to validate // x & y or nothing; zoom is optional let x = parseInt(params.get(CLIENT_PARAMS.canvas_x) || ""); let y = parseInt(params.get(CLIENT_PARAMS.canvas_y) || ""); if (!isNaN(x) && !isNaN(y)) { // x & y are valid numbers canvas = { x, y, }; if (params.has(CLIENT_PARAMS.canvas_zoom)) { let zoom = parseInt(params.get(CLIENT_PARAMS.canvas_zoom) || ""); if (!isNaN(zoom)) { canvas.zoom = zoom; } } } } let template: | { url: string; width?: number; x?: number; y?: number; } | undefined = undefined; if (params.has(CLIENT_PARAMS.template_url)) { const url = params.get(CLIENT_PARAMS.template_url)!; template = { url }; if (params.has(CLIENT_PARAMS.template_width)) { let width = parseInt(params.get(CLIENT_PARAMS.template_width) || ""); if (!isNaN(width)) { template.width = width; } } if ( params.has(CLIENT_PARAMS.template_x) && params.has(CLIENT_PARAMS.template_y) ) { // both x & y has to be set let x = parseInt(params.get(CLIENT_PARAMS.template_x) || ""); let y = parseInt(params.get(CLIENT_PARAMS.template_y) || ""); if (!isNaN(x) && !isNaN(y)) { template.x = x; template.y = y; } } } return { canvas, template, }; } /** * Accept updates to local copy of TemplateContext from React * @param args */ setTemplate(args: { enabled: boolean; width?: number; x: number; y: number; url?: string; }) { this.templateState = args; } } export const Router = new _Router(); packages/client/src/lib/routes.tsdeleted 100644 → 0 +0 −38 Original line number Diff line number Diff line import { ICanvasPosition } from "@sc07-canvas/lib/src/net"; export interface ITemplateState { url: string; width: number; x: number; y: number; opacity: number; } export const Routes = { canvas: ({ pos, template, }: { pos?: ICanvasPosition; template?: ITemplateState; }) => { const params = new URLSearchParams(); if (pos) { params.set("x", pos.x + ""); params.set("y", pos.y + ""); params.set("zoom", pos.zoom + ""); } if (template) { let { url, width, x, y, opacity } = template; params.set("template.url", url); params.set("template.width", width + ""); params.set("template.x", x + ""); params.set("template.y", y + ""); params.set("template.opacity", opacity + ""); } return "/#" + params; }, }; Loading
packages/client/src/components/CanvasWrapper.tsx +46 −63 Original line number Diff line number Diff line Loading @@ -5,9 +5,9 @@ import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer"; import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext"; import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom"; import throttle from "lodash.throttle"; import { Routes } from "../lib/routes"; import { ICanvasPosition, IPosition } from "@sc07-canvas/lib/src/net"; import { IPosition } from "@sc07-canvas/lib/src/net"; import { Template } from "./Template"; import { IRouterData, Router } from "../lib/router"; export const CanvasWrapper = () => { // to prevent safari from blurring things, use the zoom css property Loading @@ -21,79 +21,60 @@ 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); useEffect(() => { Router.PanZoom = PanZoom; // @ts-ignore window.TEST_Router = Router; }, [PanZoom]); useEffect(() => { if (!config.canvas || !canvasRef.current) return; const canvas = canvasRef.current!; const canvasInstance = new Canvas(config, canvas, PanZoom); const initAt = Date.now(); { // TODO: handle hash changes and move viewport // NOTE: this will need to be cancelled if handleViewportMove was executed recently const handleNavigate = (data: IRouterData) => { if (data.canvas) { const position = canvasInstance.canvasToPanZoomTransform( data.canvas.x, data.canvas.y ); const position = parseHashParams(canvasInstance); if (position) { PanZoom.setPosition(position, { suppressEmit: true }); } PanZoom.setPosition( { x: position.transformX, y: position.transformY, zoom: data.canvas.zoom || 0, // TODO: fit canvas to viewport instead of defaulting }, { suppressEmit: true } ); } const handleViewportMove = throttle((state: ViewportMoveEvent) => { const pos = canvasInstance.panZoomTransformToCanvas(); const canvasPosition: ICanvasPosition = { x: pos.canvasX, y: pos.canvasY, zoom: state.scale >> 0, }; setCanvasPosition(canvasPosition); // initial load const initialRouter = Router.get(); console.log( "[CanvasWrapper] Initial router data, handling navigate", initialRouter ); handleNavigate(initialRouter); window.location.replace(Routes.canvas({ pos: canvasPosition })); }, 1000); const handleViewportMove = (state: ViewportMoveEvent) => { if (Date.now() - initAt < 60 * 1000) { console.debug( "[CanvasWrapper] handleViewportMove called soon after init", Date.now() - initAt ); } Router.queueUpdate(); }; const handleCursorPos = throttle((pos: IPosition) => { if ( Loading @@ -111,11 +92,13 @@ const CanvasInner = () => { PanZoom.addListener("viewportMove", handleViewportMove); canvasInstance.on("cursorPos", handleCursorPos); Router.on("navigate", handleNavigate); return () => { canvasInstance.destroy(); PanZoom.removeListener("viewportMove", handleViewportMove); canvasInstance.off("cursorPos", handleCursorPos); Router.off("navigate", handleNavigate); }; // ! do not include canvasRef, it causes infinite re-renders Loading
packages/client/src/contexts/TemplateContext.tsx +41 −6 Original line number Diff line number Diff line import { PropsWithChildren, createContext, useContext, useState } from "react"; import { PropsWithChildren, createContext, useContext, useEffect, useState, } from "react"; import { IRouterData, Router } from "../lib/router"; interface ITemplate { /** Loading Loading @@ -35,13 +42,41 @@ const templateContext = createContext<ITemplate>({} as any); export const useTemplateContext = () => useContext(templateContext); export const TemplateContext = ({ children }: PropsWithChildren) => { const [enable, setEnable] = useState(false); const [url, setURL] = useState<string>(); const [width, setWidth] = useState<number>(); const [x, setX] = useState(0); const [y, setY] = useState(0); const routerData = Router.get(); const [enable, setEnable] = useState(!!routerData.template?.url); const [url, setURL] = useState<string | undefined>(routerData.template?.url); const [width, setWidth] = useState<number | undefined>( routerData.template?.width ); const [x, setX] = useState(routerData.template?.x || 0); const [y, setY] = useState(routerData.template?.y || 0); const [opacity, setOpacity] = useState(100); useEffect(() => { const handleNavigate = (data: IRouterData) => { if (data.template) { setEnable(true); setURL(data.template.url); setWidth(data.template.width); setX(data.template.x || 0); setY(data.template.y || 0); } else { setEnable(false); } }; Router.on("navigate", handleNavigate); return () => { Router.off("navigate", handleNavigate); }; }, []); useEffect(() => { Router.setTemplate({ enabled: enable, width, x, y, url }); Router.queueUpdate(); }, [enable, width, x, y, url]); return ( <templateContext.Provider value={{ Loading
packages/client/src/lib/router.ts 0 → 100644 +216 −0 Original line number Diff line number Diff line import { PanZoom } from "@sc07-canvas/lib/src/renderer/PanZoom"; import { Canvas } from "./canvas"; import throttle from "lodash.throttle"; import EventEmitter from "eventemitter3"; const CLIENT_PARAMS = { canvas_x: "x", canvas_y: "y", canvas_zoom: "zoom", template_url: "tu", template_width: "tw", template_x: "tx", template_y: "ty", }; export interface IRouterData { canvas?: { x: number; y: number; zoom?: number; }; template?: { url: string; width?: number; x?: number; y?: number; }; } interface RouterEvents { navigate(route: IRouterData): void; } class _Router extends EventEmitter<RouterEvents> { PanZoom: PanZoom | undefined; // React TemplateContext templateState: { enabled: boolean; width?: number; x: number; y: number; url?: string; } = { enabled: false, x: 0, y: 0, }; constructor() { super(); window.addEventListener("hashchange", this._hashChange.bind(this)); } destroy() { // NOTE: this method *never* gets called because this is intended to be global window.removeEventListener("hashchange", this._hashChange.bind(this)); } _hashChange(e: HashChangeEvent) { const data = this.get(); console.info("[Router] Navigated", data); this.emit("navigate", data); } queueUpdate = throttle(this.update, 500); update() { const url = this.getURL(); if (!url) return; console.log("[Router] Updating URL"); window.history.replaceState({}, "", url); } getURL() { const canvas = Canvas.instance; // this is not that helpful because the data is more spread out // this gets replaced by using TemplateContext data // const template = Template.instance; if (!canvas) { console.warn("Router#update called but no canvas instance exists"); return; } if (!this.PanZoom) { console.warn("Router#update called but no PanZoom instance exists"); } const params = new URLSearchParams(); const position = canvas.panZoomTransformToCanvas(); params.set(CLIENT_PARAMS.canvas_x, position.canvasX + ""); params.set(CLIENT_PARAMS.canvas_y, position.canvasY + ""); params.set( CLIENT_PARAMS.canvas_zoom, (this.PanZoom!.transform.scale >> 0) + "" ); if (this.templateState.enabled && this.templateState.url) { params.set(CLIENT_PARAMS.template_url, this.templateState.url + ""); if (this.templateState.width) params.set(CLIENT_PARAMS.template_width, this.templateState.width + ""); params.set(CLIENT_PARAMS.template_x, this.templateState.x + ""); params.set(CLIENT_PARAMS.template_y, this.templateState.y + ""); } return ( window.location.protocol + "//" + window.location.host + "/#" + params ); } /** * Parse the URL and return what was found, following specifications * There's no defaults, if it's not specified in the url, it's not specified in the return * * @returns */ get(): IRouterData { const params = new URLSearchParams(window.location.hash.slice(1)); let canvas: | { x: number; y: number; zoom?: number; } | undefined = undefined; if ( params.has(CLIENT_PARAMS.canvas_x) && params.has(CLIENT_PARAMS.canvas_y) ) { // params exist, now to validate // x & y or nothing; zoom is optional let x = parseInt(params.get(CLIENT_PARAMS.canvas_x) || ""); let y = parseInt(params.get(CLIENT_PARAMS.canvas_y) || ""); if (!isNaN(x) && !isNaN(y)) { // x & y are valid numbers canvas = { x, y, }; if (params.has(CLIENT_PARAMS.canvas_zoom)) { let zoom = parseInt(params.get(CLIENT_PARAMS.canvas_zoom) || ""); if (!isNaN(zoom)) { canvas.zoom = zoom; } } } } let template: | { url: string; width?: number; x?: number; y?: number; } | undefined = undefined; if (params.has(CLIENT_PARAMS.template_url)) { const url = params.get(CLIENT_PARAMS.template_url)!; template = { url }; if (params.has(CLIENT_PARAMS.template_width)) { let width = parseInt(params.get(CLIENT_PARAMS.template_width) || ""); if (!isNaN(width)) { template.width = width; } } if ( params.has(CLIENT_PARAMS.template_x) && params.has(CLIENT_PARAMS.template_y) ) { // both x & y has to be set let x = parseInt(params.get(CLIENT_PARAMS.template_x) || ""); let y = parseInt(params.get(CLIENT_PARAMS.template_y) || ""); if (!isNaN(x) && !isNaN(y)) { template.x = x; template.y = y; } } } return { canvas, template, }; } /** * Accept updates to local copy of TemplateContext from React * @param args */ setTemplate(args: { enabled: boolean; width?: number; x: number; y: number; url?: string; }) { this.templateState = args; } } export const Router = new _Router();
packages/client/src/lib/routes.tsdeleted 100644 → 0 +0 −38 Original line number Diff line number Diff line import { ICanvasPosition } from "@sc07-canvas/lib/src/net"; export interface ITemplateState { url: string; width: number; x: number; y: number; opacity: number; } export const Routes = { canvas: ({ pos, template, }: { pos?: ICanvasPosition; template?: ITemplateState; }) => { const params = new URLSearchParams(); if (pos) { params.set("x", pos.x + ""); params.set("y", pos.y + ""); params.set("zoom", pos.zoom + ""); } if (template) { let { url, width, x, y, opacity } = template; params.set("template.url", url); params.set("template.width", width + ""); params.set("template.x", x + ""); params.set("template.y", y + ""); params.set("template.opacity", opacity + ""); } return "/#" + params; }, };