store position in location hash, allowing resharing
This commit is contained in:
parent
d29419bcf7
commit
9ea1e903db
|
@ -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 CanvasInner = () => {
|
||||||
const canvasRef = createRef<HTMLCanvasElement>();
|
const canvasRef = createRef<HTMLCanvasElement>();
|
||||||
const { config, setCanvasPosition, setCursorPosition } = useAppContext();
|
const { config, setCanvasPosition, setCursorPosition } = useAppContext();
|
||||||
const PanZoom = useContext(RendererContext);
|
const PanZoom = useContext(RendererContext);
|
||||||
// const { centerView } = useControls();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!config.canvas || !canvasRef.current) return;
|
if (!config.canvas || !canvasRef.current) return;
|
||||||
const canvas = canvasRef.current!;
|
const canvas = canvasRef.current!;
|
||||||
const canvasInstance = new Canvas(config, canvas, PanZoom);
|
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 handleViewportMove = throttle((state: ViewportMoveEvent) => {
|
||||||
const pos = canvasInstance.panZoomTransformToCanvas();
|
const pos = canvasInstance.panZoomTransformToCanvas();
|
||||||
|
@ -45,7 +93,7 @@ const CanvasInner = () => {
|
||||||
window.location.replace(Routes.canvas(canvasPosition));
|
window.location.replace(Routes.canvas(canvasPosition));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
const handleCursorPos = (pos: IPosition) => {
|
const handleCursorPos = throttle((pos: IPosition) => {
|
||||||
if (
|
if (
|
||||||
pos.x < 0 ||
|
pos.x < 0 ||
|
||||||
pos.y < 0 ||
|
pos.y < 0 ||
|
||||||
|
@ -54,9 +102,10 @@ const CanvasInner = () => {
|
||||||
) {
|
) {
|
||||||
setCursorPosition();
|
setCursorPosition();
|
||||||
} else {
|
} else {
|
||||||
setCursorPosition(pos);
|
// fixes not passing the current value
|
||||||
|
setCursorPosition({ ...pos });
|
||||||
}
|
}
|
||||||
};
|
}, 1);
|
||||||
|
|
||||||
PanZoom.addListener("viewportMove", handleViewportMove);
|
PanZoom.addListener("viewportMove", handleViewportMove);
|
||||||
canvasInstance.on("cursorPos", handleCursorPos);
|
canvasInstance.on("cursorPos", handleCursorPos);
|
||||||
|
@ -66,7 +115,9 @@ const CanvasInner = () => {
|
||||||
PanZoom.removeListener("viewportMove", handleViewportMove);
|
PanZoom.removeListener("viewportMove", handleViewportMove);
|
||||||
canvasInstance.off("cursorPos", handleCursorPos);
|
canvasInstance.off("cursorPos", handleCursorPos);
|
||||||
};
|
};
|
||||||
}, [PanZoom, canvasRef, config, setCanvasPosition]);
|
|
||||||
|
// do not include canvasRef, it causes infinite re-renders
|
||||||
|
}, [PanZoom, config, setCanvasPosition, setCursorPosition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
|
|
|
@ -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() {
|
panZoomTransformToCanvas() {
|
||||||
const { x, y, scale: zoom } = this.PanZoom.transform;
|
const { x, y, scale: zoom } = this.PanZoom.transform;
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
|
|
@ -82,6 +82,8 @@ interface ISetup {
|
||||||
* [minimum scale, maximum scale]
|
* [minimum scale, maximum scale]
|
||||||
*/
|
*/
|
||||||
scale: [number, number];
|
scale: [number, number];
|
||||||
|
|
||||||
|
initialTransform?: TransformState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move these event interfaces out
|
// TODO: move these event interfaces out
|
||||||
|
@ -106,9 +108,12 @@ interface PanZoomEvents {
|
||||||
click: (e: ClickEvent) => void;
|
click: (e: ClickEvent) => void;
|
||||||
hover: (e: HoverEvent) => void;
|
hover: (e: HoverEvent) => void;
|
||||||
viewportMove: (e: ViewportMoveEvent) => void;
|
viewportMove: (e: ViewportMoveEvent) => void;
|
||||||
|
initialize: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanZoom extends EventEmitter<PanZoomEvents> {
|
export class PanZoom extends EventEmitter<PanZoomEvents> {
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
public $wrapper: HTMLDivElement = null as any;
|
public $wrapper: HTMLDivElement = null as any;
|
||||||
public $zoom: HTMLDivElement = null as any;
|
public $zoom: HTMLDivElement = null as any;
|
||||||
public $move: HTMLDivElement = null as any;
|
public $move: HTMLDivElement = null as any;
|
||||||
|
@ -165,6 +170,53 @@ export class PanZoom extends EventEmitter<PanZoomEvents> {
|
||||||
this.detectFlags();
|
this.detectFlags();
|
||||||
this.registerMouseEvents();
|
this.registerMouseEvents();
|
||||||
this.registerTouchEvents();
|
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() {
|
detectFlags() {
|
||||||
|
@ -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", {
|
this.emit("viewportMove", {
|
||||||
scale: this.transform.scale,
|
scale: this.transform.scale,
|
||||||
x: this.transform.x,
|
x: this.transform.x,
|
||||||
y: this.transform.y,
|
y: this.transform.y,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.flags.useZoom) {
|
if (this.flags.useZoom) {
|
||||||
this.$zoom.style.setProperty("zoom", this.transform.scale * 100 + "%");
|
this.$zoom.style.setProperty("zoom", this.transform.scale * 100 + "%");
|
||||||
|
|
Loading…
Reference in New Issue