Commit 9ea1e903 authored by Grant's avatar Grant
Browse files

store position in location hash, allowing resharing

parent d29419bc
Loading
Loading
Loading
Loading
+57 −6
Original line number Diff line number Diff line
@@ -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
+16 −0
Original line number Diff line number Diff line
@@ -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();
+74 −6
Original line number Diff line number Diff line
@@ -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() {
  /**
   * 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 + "%");