Skip to content
CanvasWrapper.tsx 6.29 KiB
Newer Older
Grant's avatar
Grant committed
import { useCallback, useContext, useEffect, useRef } from "react";
Grant's avatar
Grant committed
import { Canvas } from "../lib/canvas";
import { useAppContext } from "../contexts/AppContext";
import { PanZoomWrapper } from "@sc07-canvas/lib/src/renderer";
Grant's avatar
Grant committed
import { RendererContext } from "@sc07-canvas/lib/src/renderer/RendererContext";
Grant's avatar
Grant committed
import { ViewportMoveEvent } from "@sc07-canvas/lib/src/renderer/PanZoom";
import throttle from "lodash.throttle";
Grant's avatar
Grant committed
import { IPosition } from "@sc07-canvas/lib/src/net";
import { Template } from "./Templating/Template";
Grant's avatar
Grant committed
import { IRouterData, Router } from "../lib/router";
import { KeybindManager } from "../lib/keybinds";
Grant's avatar
Grant committed
import { BlankOverlay } from "./Overlay/BlankOverlay";
import { HeatmapOverlay } from "./Overlay/HeatmapOverlay";
Grant's avatar
Grant committed

export const CanvasWrapper = () => {
  const { config } = useAppContext();
Grant's avatar
Grant committed
  // to prevent safari from blurring things, use the zoom css property
Grant's avatar
Grant committed
  return (
    <main>
      <PanZoomWrapper>
Grant's avatar
Grant committed
        <BlankOverlay />
        <CanvasInner />
      </PanZoomWrapper>
Grant's avatar
Grant committed
    </main>
  );
};

const CanvasInner = () => {
Grant's avatar
Grant committed
  const canvasRef = useRef<HTMLCanvasElement | null>();
  const canvas = useRef<Canvas>();
  const { config, setCanvasPosition, setCursorPosition, setPixelWhois } =
    useAppContext();
Grant's avatar
Grant committed
  const PanZoom = useContext(RendererContext);
Grant's avatar
Grant committed

Grant's avatar
Grant committed
  useEffect(() => {
    if (!canvasRef.current) return;
    canvas.current = new Canvas(canvasRef.current!, PanZoom);

    const handlePixelWhois = ({
      clientX,
      clientY,
    }: {
      clientX: number;
      clientY: number;
    }) => {
      if (!canvas.current) {
        console.warn(
          "[CanvasWrapper#handlePixelWhois] canvas instance does not exist"
        );
        return;
      }

      const [x, y] = canvas.current.screenToPos(clientX, clientY);
      if (x < 0 || y < 0) return; // discard if out of bounds

      // canvas size can dynamically change, so we need to check the current config
      // we're depending on canvas.instance's config so we don't have to use a react dependency
      if (canvas.current.hasConfig()) {
        const {
          canvas: {
            size: [width, height],
          },
        } = canvas.current.getConfig();

        if (x >= width || y >= height) return; // out of bounds
      } else {
        // although this should never happen, log it
        console.warn(
          "[CanvasWrapper#handlePixelWhois] canvas config is not available yet"
        );
      }

      // .......
      // .......
      // .......
      // ...x...
      // .......
      // .......
      // .......
      const surrounding = canvas.current.getSurroundingPixels(x, y, 3);

      setPixelWhois({ x, y, surrounding });
    };

    KeybindManager.on("PIXEL_WHOIS", handlePixelWhois);

Grant's avatar
Grant committed
    return () => {
      KeybindManager.off("PIXEL_WHOIS", handlePixelWhois);
Grant's avatar
Grant committed
      canvas.current!.destroy();
    };
  }, [PanZoom, setCursorPosition]);

Grant's avatar
Grant committed
  useEffect(() => {
    Router.PanZoom = PanZoom;
  }, [PanZoom]);

Grant's avatar
Grant committed
  useEffect(() => {
Grant's avatar
Grant committed
    if (!canvas.current) {
      console.warn("canvas isntance doesn't exist");
      return;
    }

    const handleCursorPos = throttle((pos: IPosition) => {
      if (!canvas.current?.hasConfig() || !config) {
        console.warn("handleCursorPos has no config");
        return;
      }

      if (
        pos.x < 0 ||
        pos.y < 0 ||
        pos.x > config.canvas.size[0] ||
        pos.y > config.canvas.size[1]
      ) {
        setCursorPosition();
      } else {
        // fixes not passing the current value
        setCursorPosition({ ...pos });
      }
    }, 1);

    canvas.current.on("cursorPos", handleCursorPos);

    return () => {
      canvas.current!.off("cursorPos", handleCursorPos);
    };
  }, [config, setCursorPosition]);

  useEffect(() => {
    if (!canvas.current) {
      console.warn("canvasinner config received but no canvas instance");
      return;
    }
    if (!config) {
      console.warn("canvasinner config received falsey");
      return;
    }
Grant's avatar
Grant committed
    console.log("[CanvasInner] config updated, informing canvas instance");
    canvas.current.loadConfig(config);

    // refresh because canvas might've resized
    const initialRouter = Router.get();
    console.log(
      "[CanvasWrapper] Config updated, triggering navigate",
      initialRouter
    );
    handleNavigate(initialRouter);
  }, [config]);

  const handleNavigate = useCallback(
    (data: IRouterData) => {
Grant's avatar
Grant committed
      if (data.canvas) {
Grant's avatar
Grant committed
        const position = canvas.current!.canvasToPanZoomTransform(
Grant's avatar
Grant committed
          data.canvas.x,
          data.canvas.y
        );

        PanZoom.setPosition(
          {
            x: position.transformX,
            y: position.transformY,
            zoom: data.canvas.zoom || 0, // TODO: fit canvas to viewport instead of defaulting
          },
          { suppressEmit: true }
        );
Grant's avatar
Grant committed
    },
    [PanZoom]
  );

  useEffect(() => {
    // if (!config?.canvas || !canvasRef.current) return;
    // const canvas = canvasRef.current!;
    // const canvasInstance = new Canvas(canvas, PanZoom);
    const initAt = Date.now();
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    // initial load
    const initialRouter = Router.get();
    console.log(
      "[CanvasWrapper] Initial router data, handling navigate",
      initialRouter
    );
    handleNavigate(initialRouter);

    const handleViewportMove = (state: ViewportMoveEvent) => {
      if (Date.now() - initAt < 60 * 1000) {
        console.debug(
          "[CanvasWrapper] handleViewportMove called soon after init",
          Date.now() - initAt
        );
      }
Grant's avatar
Grant committed

      if (canvas.current) {
        const pos = canvas.current?.panZoomTransformToCanvas();
        setCanvasPosition({
          x: pos.canvasX,
          y: pos.canvasY,
          zoom: state.scale >> 0,
        });
      } else {
        console.warn(
          "[CanvasWrapper] handleViewportMove has no canvas instance"
        );
      }

Grant's avatar
Grant committed
      Router.queueUpdate();
    };
Grant's avatar
Grant committed

    PanZoom.addListener("viewportMove", handleViewportMove);
Grant's avatar
Grant committed
    Router.on("navigate", handleNavigate);
Grant's avatar
Grant committed

Grant's avatar
Grant committed
    return () => {
Grant's avatar
Grant committed
      PanZoom.removeListener("viewportMove", handleViewportMove);
Grant's avatar
Grant committed
      Router.off("navigate", handleNavigate);
Grant's avatar
Grant committed
    };
Grant's avatar
Grant committed
  }, [PanZoom, setCanvasPosition, setCursorPosition]);
Grant's avatar
Grant committed

  return (
    <canvas
      id="board"
      width="1000"
      height="1000"
      className="pixelate"
Grant's avatar
Grant committed
      ref={(ref) => (canvasRef.current = ref)}
Grant's avatar
Grant committed
    ></canvas>
  );
};