store position in location hash, allowing resharing

This commit is contained in:
Grant 2024-02-22 22:00:52 -07:00
parent d29419bcf7
commit 9ea1e903db
3 changed files with 147 additions and 12 deletions

View File

@ -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();
@ -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 ||
@ -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);
@ -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

View File

@ -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();

View File

@ -82,6 +82,8 @@ interface ISetup {
* [minimum scale, maximum scale]
*/
scale: [number, number];
initialTransform?: TransformState;
}
// TODO: move these event interfaces out
@ -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;
@ -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() {
@ -453,12 +505,28 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
);
}
update() {
this.emit("viewportMove", {
scale: this.transform.scale,
x: this.transform.x,
y: this.transform.y,
});
/**
* 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 + "%");