Commit 5a30f3bd authored by Grant's avatar Grant
Browse files

implement heatmap (fixes #31), fix a typo in palette, add opacity slider to virginmap

parent b38b1d8b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import { Template } from "./Template";
import { IRouterData, Router } from "../lib/router";
import { KeybindManager } from "../lib/keybinds";
import { VirginOverlay } from "./Overlay/VirginOverlay";
import { HeatmapOverlay } from "./Overlay/HeatmapOverlay";

export const CanvasWrapper = () => {
  const { config } = useAppContext();
@@ -19,6 +20,7 @@ export const CanvasWrapper = () => {
    <main>
      <PanZoomWrapper>
        <VirginOverlay />
        <HeatmapOverlay />
        {config && <Template />}
        <CanvasInner />
      </PanZoomWrapper>
+154 −0
Original line number Diff line number Diff line
import { useCallback, useEffect, useRef } from "react";
import { useAppContext } from "../../contexts/AppContext";
import { KeybindManager } from "../../lib/keybinds";
import { api } from "../../lib/utils";
import { toast } from "react-toastify";
import network from "../../lib/network";

export const HeatmapOverlay = () => {
  const { config, heatmapOverlay, setHeatmapOverlay } = useAppContext();
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const handleKeybind = () => {
      setHeatmapOverlay((v) => ({ ...v, enabled: !v.enabled }));
    };

    KeybindManager.on("TOGGLE_HEATMAP", handleKeybind);

    return () => {
      KeybindManager.off("TOGGLE_HEATMAP", handleKeybind);
    };
  }, [setHeatmapOverlay]);

  useEffect(() => {
    if (!config) {
      console.warn("[HeatmapOverlay] config is not defined");
      return;
    }
    if (!canvasRef.current) {
      console.warn("[HeatmapOverlay] canvasRef is not defined");
      return;
    }

    const [width, height] = config.canvas.size;

    canvasRef.current.width = width;
    canvasRef.current.height = height;
  }, [config]);

  const drawHeatmap = useCallback(
    (rawData: string) => {
      console.debug("[HeatmapOverlay] drawing heatmap");
      if (!config) {
        console.warn("[HeatmapOverlay] no config instance available");
        return;
      }

      const ctx = canvasRef.current!.getContext("2d");
      if (!ctx) {
        console.warn("[HeatmapOverlay] canvas context cannot be aquired");
        return;
      }

      ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);

      if (heatmapOverlay.enabled) {
        let heatmap = rawData.split("");
        let lines: number[][] = [];

        while (heatmap.length > 0) {
          // each pixel is stored as 2 characters
          let line = heatmap.splice(0, config?.canvas.size[0] * 2).join("");
          let pixels = (line.match(/.{1,2}/g) || []).map(
            (v) => parseInt(v, 36) / 100
          );

          lines.push(pixels);
        }

        for (let y = 0; y < lines.length; y++) {
          for (let x = 0; x < lines[y].length; x++) {
            const val = lines[y][x];

            ctx.fillStyle = `rgba(255, 0, 0, ${Math.max(val, 0.1).toFixed(2)})`;
            ctx.fillRect(x, y, 1, 1);
          }
        }
      } else {
        console.warn(
          "[HeatmapOverlay] drawHeatmap called with heatmap disabled"
        );
      }
    },
    [config, heatmapOverlay.enabled]
  );

  const updateHeatmap = useCallback(() => {
    setHeatmapOverlay((v) => ({ ...v, loading: true }));

    api<{ heatmap: string }, "heatmap_not_generated">("/api/heatmap")
      .then(({ status, data }) => {
        if (status === 200 && data.success) {
          drawHeatmap(data.heatmap);
        } else {
          if ("error" in data) {
            switch (data.error) {
              case "heatmap_not_generated":
                toast.info("Heatmap is not generated. Try again shortly");
                setHeatmapOverlay((v) => ({ ...v, enabled: false }));
                break;
              default:
                toast.error("Unknown error: " + data.error);
            }
          } else {
            toast.error("Failed to load heatmap: Error " + status);
          }
        }
      })
      .finally(() => {
        setHeatmapOverlay((v) => ({ ...v, loading: false }));
      });
  }, [drawHeatmap, setHeatmapOverlay]);

  useEffect(() => {
    if (!canvasRef.current) {
      console.warn("[HeatmapOverlay] canvasRef is not defined");
      return;
    }

    updateHeatmap();

    return () => {};
  }, [canvasRef, heatmapOverlay.enabled, updateHeatmap]);

  useEffect(() => {
    if (heatmapOverlay.enabled) {
      console.debug("[HeatmapOverlay] subscribing to heatmap updates");
      network.subscribe("heatmap");
    } else {
      console.debug("[HeatmapOverlay] unsubscribing from heatmap updates");
      network.unsubscribe("heatmap");
    }

    network.on("heatmap", drawHeatmap);

    return () => {
      network.off("heatmap", drawHeatmap);
    };
  }, [drawHeatmap, heatmapOverlay.enabled]);

  return (
    <canvas
      id="heatmap-overlay"
      className="board-overlay no-interact pixelate"
      ref={(r) => (canvasRef.current = r)}
      width="1000"
      height="1000"
      style={{
        display: heatmapOverlay.enabled ? "block" : "none",
        opacity: heatmapOverlay.opacity.toFixed(1),
      }}
    />
  );
};
+43 −4
Original line number Diff line number Diff line
import { Switch } from "@nextui-org/react";
import { Slider, Spinner, Switch } from "@nextui-org/react";
import { useAppContext } from "../../contexts/AppContext";

export const OverlaySettings = () => {
  const { showVirginOverlay, setShowVirginOverlay } = useAppContext();
  const { virginOverlay, setVirginOverlay, heatmapOverlay, setHeatmapOverlay } =
    useAppContext();

  return (
    <>
@@ -11,11 +12,49 @@ export const OverlaySettings = () => {
      </header>
      <section>
        <Switch
          isSelected={showVirginOverlay}
          onValueChange={setShowVirginOverlay}
          isSelected={virginOverlay.enabled}
          onValueChange={(v) =>
            setVirginOverlay((vv) => ({ ...vv, enabled: v }))
          }
        >
          Virgin Map Overlay
        </Switch>
        {virginOverlay.enabled && (
          <Slider
            label="Virgin Map Opacity"
            step={0.1}
            minValue={0}
            maxValue={1}
            value={virginOverlay.opacity}
            onChange={(v) =>
              setVirginOverlay((vv) => ({ ...vv, opacity: v as number }))
            }
            getValue={(v) => (v as number) * 100 + "%"}
          />
        )}

        <Switch
          isSelected={heatmapOverlay.enabled}
          onValueChange={(v) =>
            setHeatmapOverlay((vv) => ({ ...vv, enabled: v }))
          }
        >
          {heatmapOverlay.loading && <Spinner size="sm" />}
          Heatmap Overlay
        </Switch>
        {heatmapOverlay.enabled && (
          <Slider
            label="Heatmap Opacity"
            step={0.1}
            minValue={0}
            maxValue={1}
            value={heatmapOverlay.opacity}
            onChange={(v) =>
              setHeatmapOverlay((vv) => ({ ...vv, opacity: v as number }))
            }
            getValue={(v) => (v as number) * 100 + "%"}
          />
        )}
      </section>
    </>
  );
+5 −4
Original line number Diff line number Diff line
@@ -4,12 +4,12 @@ import { Canvas } from "../../lib/canvas";
import { KeybindManager } from "../../lib/keybinds";

export const VirginOverlay = () => {
  const { config, showVirginOverlay, setShowVirginOverlay } = useAppContext();
  const { config, virginOverlay, setVirginOverlay } = useAppContext();
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const handleKeybind = () => {
      setShowVirginOverlay((v) => !v);
      setVirginOverlay((v) => ({ ...v, enabled: !v.enabled }));
    };

    KeybindManager.on("TOGGLE_VIRGIN", handleKeybind);
@@ -17,7 +17,7 @@ export const VirginOverlay = () => {
    return () => {
      KeybindManager.off("TOGGLE_VIRGIN", handleKeybind);
    };
  }, [setShowVirginOverlay]);
  }, [setVirginOverlay]);

  useEffect(() => {
    if (!config) {
@@ -74,7 +74,8 @@ export const VirginOverlay = () => {
      width="1000"
      height="1000"
      style={{
        display: showVirginOverlay ? "block" : "none",
        display: virginOverlay.enabled ? "block" : "none",
        opacity: virginOverlay.opacity.toFixed(1),
      }}
    />
  );
+2 −2
Original line number Diff line number Diff line
@@ -3,11 +3,11 @@ import { useAppContext } from "../../contexts/AppContext";
import { Canvas } from "../../lib/canvas";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { IPalleteContext } from "@sc07-canvas/lib/src/net";
import { IPaletteContext } from "@sc07-canvas/lib/src/net";

export const Palette = () => {
  const { config, user } = useAppContext();
  const [pallete, setPallete] = useState<IPalleteContext>({});
  const [pallete, setPallete] = useState<IPaletteContext>({});

  useEffect(() => {
    if (!Canvas.instance) return;
Loading