Loading packages/client/src/components/CanvasWrapper.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -19,6 +20,7 @@ export const CanvasWrapper = () => { <main> <PanZoomWrapper> <VirginOverlay /> <HeatmapOverlay /> {config && <Template />} <CanvasInner /> </PanZoomWrapper> Loading packages/client/src/components/Overlay/HeatmapOverlay.tsx 0 → 100644 +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), }} /> ); }; packages/client/src/components/Overlay/OverlaySettings.tsx +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 ( <> Loading @@ -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> </> ); Loading packages/client/src/components/Overlay/VirginOverlay.tsx +5 −4 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -17,7 +17,7 @@ export const VirginOverlay = () => { return () => { KeybindManager.off("TOGGLE_VIRGIN", handleKeybind); }; }, [setShowVirginOverlay]); }, [setVirginOverlay]); useEffect(() => { if (!config) { Loading Loading @@ -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), }} /> ); Loading packages/client/src/components/Toolbar/Palette.tsx +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/client/src/components/CanvasWrapper.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -19,6 +20,7 @@ export const CanvasWrapper = () => { <main> <PanZoomWrapper> <VirginOverlay /> <HeatmapOverlay /> {config && <Template />} <CanvasInner /> </PanZoomWrapper> Loading
packages/client/src/components/Overlay/HeatmapOverlay.tsx 0 → 100644 +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), }} /> ); };
packages/client/src/components/Overlay/OverlaySettings.tsx +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 ( <> Loading @@ -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> </> ); Loading
packages/client/src/components/Overlay/VirginOverlay.tsx +5 −4 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -17,7 +17,7 @@ export const VirginOverlay = () => { return () => { KeybindManager.off("TOGGLE_VIRGIN", handleKeybind); }; }, [setShowVirginOverlay]); }, [setVirginOverlay]); useEffect(() => { if (!config) { Loading Loading @@ -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), }} /> ); Loading
packages/client/src/components/Toolbar/Palette.tsx +2 −2 Original line number Diff line number Diff line Loading @@ -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