Skip to content
CanvasWrapper.tsx 9.58 KiB
Newer Older
Grant's avatar
Grant committed
import { useCallback, useContext, useEffect, useRef, useState } 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";
import { Template as TemplateCl } from "../lib/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";
import { useTemplateContext } from "../contexts/TemplateContext";
import { PixelPulses } from "./Overlay/PixelPulses";
import { CanvasUtils } from "../lib/canvas.utils";
Grant's avatar
Grant committed

export const CanvasWrapper = () => {
  const { config } = useAppContext();

  const getInitialPosition = useCallback<
    (useCssZoom: boolean) =>
      | {
          x: number;
          y: number;
          zoom?: number;
        }
      | undefined
  >(
    (useCssZoom) => {
      const router = Router.get().canvas;

      if (!router) return undefined;
      if (!config) {
        console.warn("getInitialPosition called with no config");
        return undefined;
      }

      const { transformX, transformY } = CanvasUtils.canvasToPanZoomTransform(
        router.x,
        router.y,
        config.canvas.size,
        useCssZoom
      );

      return {
        x: transformX,
        y: transformY,
        zoom: router.zoom,
      };
    },
    [config]
  );
Grant's avatar
Grant committed
  return (
    <main>
      <PanZoomWrapper initialPosition={getInitialPosition}>
Grant's avatar
Grant committed
        <BlankOverlay />
        <PixelPulses />
        <CanvasInner />
Grant's avatar
Grant committed
        <Cursor />
      </PanZoomWrapper>
Grant's avatar
Grant committed
    </main>
  );
};

Grant's avatar
Grant committed
const Cursor = () => {
  const { cursor } = useAppContext();
  const [color, setColor] = useState<string>();

  useEffect(() => {
    if (typeof cursor.color === "number") {
      const color = Canvas.instance?.Pallete.getColor(cursor.color);
      setColor(color?.hex);
    } else {
      setColor(undefined);
    }
  }, [setColor, cursor.color]);

  if (!color) return <></>;

  return (
    <div
      className="noselect"
      style={{
        position: "absolute",
        top: cursor.y,
        left: cursor.x,
        backgroundColor: "#" + color,
        width: "1px",
        height: "1px",
        opacity: 0.5,
      }}
    ></div>
  );
};

Grant's avatar
Grant committed
const CanvasInner = () => {
Grant's avatar
Grant committed
  const canvasRef = useRef<HTMLCanvasElement | null>();
  const canvas = useRef<Canvas>();
Grant's avatar
Grant committed
  const { config, setCanvasPosition, setCursor, setPixelWhois } =
  const {
    x: templateX,
    y: templateY,
    enable: templateEnable,
  } = useTemplateContext();
Grant's avatar
Grant committed
  const PanZoom = useContext(RendererContext);
Grant's avatar
Grant committed

Grant's avatar
Grant committed
  /**
   * Is the canvas coordinate within the bounds of the canvas?
   */
  const isCoordInCanvas = useCallback(
    (x: number, y: number): boolean => {
Grant's avatar
Grant committed
          "[CanvasWrapper#isCoordInCanvas] canvas instance does not exist"
Grant's avatar
Grant committed
        return false;
Grant's avatar
Grant committed
      if (x < 0 || y < 0) return false; // not positive, impossible to be on canvas

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

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

      return true;
    },
    [canvas.current]
  );

  const handlePixelWhois = useCallback(
    ({ clientX, clientY }: { clientX: number; clientY: number }) => {
      if (!canvas.current) {
        console.warn(
          "[CanvasWrapper#handlePixelWhois] canvas instance does not exist"
Grant's avatar
Grant committed
        return;
Grant's avatar
Grant committed
      const [x, y] = canvas.current.screenToPos(clientX, clientY);
      if (!isCoordInCanvas(x, y)) return; // out of bounds

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

      setPixelWhois({ x, y, surrounding });
Grant's avatar
Grant committed
    },
    [canvas.current]
  );

  const getTemplatePixel = useCallback(
    (x: number, y: number) => {
      if (!templateEnable) return;
      if (x < templateX || y < templateY) return;

      x -= templateX;
      y -= templateY;

      return TemplateCl.instance.getPixel(x, y);
    },
    [templateX, templateY]
  );

Grant's avatar
Grant committed
  const handlePickPixel = useCallback(
    ({ clientX, clientY }: { clientX: number; clientY: number }) => {
      if (!canvas.current) {
        console.warn(
          "[CanvasWrapper#handlePickPixel] canvas instance does not exist"
        );
        return;
      }

      const [x, y] = canvas.current.screenToPos(clientX, clientY);
      if (!isCoordInCanvas(x, y)) return; // out of bounds

      let pixelColor = -1;

      const templatePixel = getTemplatePixel(x, y);
      if (templatePixel) {
        pixelColor =
          canvas.current.Pallete.getColorFromHex(templatePixel.slice(1))?.id ||
          -1;
      }

      if (pixelColor === -1) {
        pixelColor = canvas.current.getPixel(x, y)?.color || -1;
      }

      if (pixelColor === -1) {
        return;
      }
Grant's avatar
Grant committed

      // no need to use canvas#setCursor as Palette.tsx already does that
      setCursor((v) => ({
        ...v,
        color: pixelColor,
Grant's avatar
Grant committed
      }));
    },
    [canvas.current]
  );

Grant's avatar
Grant committed
  useEffect(() => {
    if (!canvasRef.current) return;
    canvas.current = new Canvas(canvasRef.current!, PanZoom);
    canvas.current.on("canvasReady", () => {
      console.log("[CanvasWrapper] received canvasReady");
    });

    KeybindManager.on("PIXEL_WHOIS", handlePixelWhois);
Grant's avatar
Grant committed
    KeybindManager.on("PICK_COLOR", handlePickPixel);
Grant's avatar
Grant committed
    return () => {
      KeybindManager.off("PIXEL_WHOIS", handlePixelWhois);
Grant's avatar
Grant committed
      KeybindManager.off("PICK_COLOR", handlePickPixel);
Grant's avatar
Grant committed
      canvas.current!.destroy();
    };
Grant's avatar
Grant committed
  }, [PanZoom]);
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]
      ) {
Grant's avatar
Grant committed
        setCursor((v) => ({
          ...v,
          x: undefined,
          y: undefined,
        }));
Grant's avatar
Grant committed
      } else {
        // fixes not passing the current value
Grant's avatar
Grant committed
        setCursor((v) => ({
          ...v,
          x: pos.x,
          y: pos.y,
        }));
Grant's avatar
Grant committed
      }
    }, 1);

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

    return () => {
      canvas.current!.off("cursorPos", handleCursorPos);
    };
Grant's avatar
Grant committed
  }, [config, setCursor]);
Grant's avatar
Grant committed

  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);
  }, [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

    // initial position from Router is setup in <CanvasWrapper>
Grant's avatar
Grant committed

    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]);
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>
  );
};