Loading packages/client/src/components/CanvasWrapper.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import { CanvasUtils } from "../lib/canvas.utils"; import { ModCanvasOverlay } from "../Moderator/ModCanvasOverlay"; import { useHasRole } from "../hooks/useHasRole"; import { toast } from "react-toastify"; import { GridOverlay } from "./Overlay/GridOverlay"; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); Loading Loading @@ -63,6 +64,7 @@ export const CanvasWrapper = () => { {hasMod && <ModCanvasOverlay />} <BlankOverlay /> <HeatmapOverlay /> <GridOverlay /> <PixelPulses /> {config && <Template />} <CanvasInner /> Loading packages/client/src/components/Overlay/GridOverlay.tsx 0 → 100644 +60 −0 Original line number Diff line number Diff line import { useEffect, useRef } from "react"; import { useAppContext } from "../../contexts/AppContext"; import { KeybindManager } from "../../lib/keybinds"; const GRID_SPACING = 8; export const GridOverlay = () => { const { gridOverlay, setGridOverlay, config } = useAppContext(); const canvasRef = useRef<HTMLCanvasElement | null>(null); useEffect(() => { const handleKeybind = () => { setGridOverlay((v) => ({ ...v, enabled: !v.enabled })); }; KeybindManager.on("TOGGLE_GRID", handleKeybind); return () => { KeybindManager.off("TOGGLE_GRID", handleKeybind); }; }, [setGridOverlay]); useEffect(() => { const canvas = canvasRef.current; if (!canvas || !gridOverlay.enabled) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; // full black for (let x = 0; x <= canvas.width; x += GRID_SPACING) { ctx.fillRect(x, 0, 1, canvas.height); // vertical line as 1px wide column } for (let y = 0; y <= canvas.height; y += GRID_SPACING) { ctx.fillRect(0, y, canvas.width, 1); // horizontal line as 1px tall row } }, [gridOverlay.enabled]); return ( <canvas id="grid-overlay" className="no-interact pixelate" ref={canvasRef} width={config.canvas.size[0] * 8} height={config.canvas.size[1] * 8} style={{ position: "absolute", top: 0, left: 0, width: config.canvas.size[0], height: config.canvas.size[1], display: gridOverlay.enabled ? "block" : "none", opacity: gridOverlay.opacity?.toFixed(1) ?? "1", pointerEvents: "none", }} /> ); }; packages/client/src/components/Overlay/OverlaySettings.tsx +25 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,8 @@ export const OverlaySettings = () => { setHeatmapOverlay, pixelPulses, setPixelPulses, gridOverlay, setGridOverlay, } = useAppContext(); return ( Loading Loading @@ -66,6 +68,29 @@ export const OverlaySettings = () => { /> )} <Switch isSelected={gridOverlay.enabled} onValueChange={(v) => { setGridOverlay((vv) => ({ ...vv, enabled: v })); }} > {gridOverlay.loading && <Spinner size="sm" />} Grid Overlay </Switch> {gridOverlay.enabled && ( <Slider label="Grid Opacity" step={0.025} minValue={0} maxValue={1} value={gridOverlay.opacity} onChange={(v) => setGridOverlay((vv) => ({ ...vv, opacity: v as number })) } getValue={(v) => (v as number) * 100 + "%"} /> )} <Switch isSelected={pixelPulses} onValueChange={(v) => { Loading packages/client/src/components/Settings/AudioSettings.tsx +7 −7 Original line number Diff line number Diff line Loading @@ -187,7 +187,7 @@ export const AudioSettings = () => { </header> <section className="flex flex-col gap-2"> <Switch stopSound silent isSelected={pixelAvailableSound} onValueChange={(v) => { setPixelAvailableSound(v); Loading @@ -199,7 +199,7 @@ export const AudioSettings = () => { Pixel Available </Switch> <Switch stopSound silent isSelected={placeSound} onValueChange={(v) => { setPlaceSound(v); Loading @@ -211,7 +211,7 @@ export const AudioSettings = () => { Pixel Place </Switch> <Switch stopSound silent isSelected={pixelUndoSound} onValueChange={(v) => { setPixelUndoSound(v); Loading @@ -223,7 +223,7 @@ export const AudioSettings = () => { Pixel Undo </Switch> <Switch stopSound silent isSelected={uiClickSound} onValueChange={(v) => { setUiClickSound(v); Loading @@ -235,7 +235,7 @@ export const AudioSettings = () => { UI Click </Switch> <Switch stopSound silent isSelected={disconnectSound} onValueChange={(v) => { setDisconnectSound(v); Loading @@ -247,7 +247,7 @@ export const AudioSettings = () => { Disconnect </Switch> <Switch stopSound silent isSelected={reconnectSound} onValueChange={(v) => { setReconnectSound(v); Loading @@ -259,7 +259,7 @@ export const AudioSettings = () => { Reconnect </Switch> <Switch stopSound silent isSelected={globalPlaceSound} onValueChange={(v) => { setGlobalPlaceSound(v); Loading packages/client/src/components/Settings/TemplateSettings.tsx +19 −0 Original line number Diff line number Diff line import { useTemplateContext } from "../../contexts/TemplateContext"; import { Input, Select, SelectItem, Slider } from "@nextui-org/react"; import { Switch } from "../core/Switch"; import { Button } from "../core/Button"; import { toast } from "react-toastify"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faLink } from "@fortawesome/free-solid-svg-icons"; export const TemplateSettings = () => { const { Loading Loading @@ -109,6 +113,21 @@ export const TemplateSettings = () => { > Show Mobile Tools </Switch> {url && ( <Button onPress={() => { const copied = `${window.location.origin}${window.location.pathname}#tu=${url}${ width != null ? `&tw=${width}` : "" }&tx=${x}&ty=${y}&ts=${style}&x=${x + (width || 0) / 2}&y=${y + (width || 0) / 2}&zoom=1`; navigator.clipboard.writeText(copied); toast.success("Link copied to clipboard!"); }} startContent={<FontAwesomeIcon icon={faLink} />} > Copy Shareable Link </Button> )} </section> )} </div> Loading Loading
packages/client/src/components/CanvasWrapper.tsx +2 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import { CanvasUtils } from "../lib/canvas.utils"; import { ModCanvasOverlay } from "../Moderator/ModCanvasOverlay"; import { useHasRole } from "../hooks/useHasRole"; import { toast } from "react-toastify"; import { GridOverlay } from "./Overlay/GridOverlay"; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); Loading Loading @@ -63,6 +64,7 @@ export const CanvasWrapper = () => { {hasMod && <ModCanvasOverlay />} <BlankOverlay /> <HeatmapOverlay /> <GridOverlay /> <PixelPulses /> {config && <Template />} <CanvasInner /> Loading
packages/client/src/components/Overlay/GridOverlay.tsx 0 → 100644 +60 −0 Original line number Diff line number Diff line import { useEffect, useRef } from "react"; import { useAppContext } from "../../contexts/AppContext"; import { KeybindManager } from "../../lib/keybinds"; const GRID_SPACING = 8; export const GridOverlay = () => { const { gridOverlay, setGridOverlay, config } = useAppContext(); const canvasRef = useRef<HTMLCanvasElement | null>(null); useEffect(() => { const handleKeybind = () => { setGridOverlay((v) => ({ ...v, enabled: !v.enabled })); }; KeybindManager.on("TOGGLE_GRID", handleKeybind); return () => { KeybindManager.off("TOGGLE_GRID", handleKeybind); }; }, [setGridOverlay]); useEffect(() => { const canvas = canvasRef.current; if (!canvas || !gridOverlay.enabled) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "black"; // full black for (let x = 0; x <= canvas.width; x += GRID_SPACING) { ctx.fillRect(x, 0, 1, canvas.height); // vertical line as 1px wide column } for (let y = 0; y <= canvas.height; y += GRID_SPACING) { ctx.fillRect(0, y, canvas.width, 1); // horizontal line as 1px tall row } }, [gridOverlay.enabled]); return ( <canvas id="grid-overlay" className="no-interact pixelate" ref={canvasRef} width={config.canvas.size[0] * 8} height={config.canvas.size[1] * 8} style={{ position: "absolute", top: 0, left: 0, width: config.canvas.size[0], height: config.canvas.size[1], display: gridOverlay.enabled ? "block" : "none", opacity: gridOverlay.opacity?.toFixed(1) ?? "1", pointerEvents: "none", }} /> ); };
packages/client/src/components/Overlay/OverlaySettings.tsx +25 −0 Original line number Diff line number Diff line Loading @@ -10,6 +10,8 @@ export const OverlaySettings = () => { setHeatmapOverlay, pixelPulses, setPixelPulses, gridOverlay, setGridOverlay, } = useAppContext(); return ( Loading Loading @@ -66,6 +68,29 @@ export const OverlaySettings = () => { /> )} <Switch isSelected={gridOverlay.enabled} onValueChange={(v) => { setGridOverlay((vv) => ({ ...vv, enabled: v })); }} > {gridOverlay.loading && <Spinner size="sm" />} Grid Overlay </Switch> {gridOverlay.enabled && ( <Slider label="Grid Opacity" step={0.025} minValue={0} maxValue={1} value={gridOverlay.opacity} onChange={(v) => setGridOverlay((vv) => ({ ...vv, opacity: v as number })) } getValue={(v) => (v as number) * 100 + "%"} /> )} <Switch isSelected={pixelPulses} onValueChange={(v) => { Loading
packages/client/src/components/Settings/AudioSettings.tsx +7 −7 Original line number Diff line number Diff line Loading @@ -187,7 +187,7 @@ export const AudioSettings = () => { </header> <section className="flex flex-col gap-2"> <Switch stopSound silent isSelected={pixelAvailableSound} onValueChange={(v) => { setPixelAvailableSound(v); Loading @@ -199,7 +199,7 @@ export const AudioSettings = () => { Pixel Available </Switch> <Switch stopSound silent isSelected={placeSound} onValueChange={(v) => { setPlaceSound(v); Loading @@ -211,7 +211,7 @@ export const AudioSettings = () => { Pixel Place </Switch> <Switch stopSound silent isSelected={pixelUndoSound} onValueChange={(v) => { setPixelUndoSound(v); Loading @@ -223,7 +223,7 @@ export const AudioSettings = () => { Pixel Undo </Switch> <Switch stopSound silent isSelected={uiClickSound} onValueChange={(v) => { setUiClickSound(v); Loading @@ -235,7 +235,7 @@ export const AudioSettings = () => { UI Click </Switch> <Switch stopSound silent isSelected={disconnectSound} onValueChange={(v) => { setDisconnectSound(v); Loading @@ -247,7 +247,7 @@ export const AudioSettings = () => { Disconnect </Switch> <Switch stopSound silent isSelected={reconnectSound} onValueChange={(v) => { setReconnectSound(v); Loading @@ -259,7 +259,7 @@ export const AudioSettings = () => { Reconnect </Switch> <Switch stopSound silent isSelected={globalPlaceSound} onValueChange={(v) => { setGlobalPlaceSound(v); Loading
packages/client/src/components/Settings/TemplateSettings.tsx +19 −0 Original line number Diff line number Diff line import { useTemplateContext } from "../../contexts/TemplateContext"; import { Input, Select, SelectItem, Slider } from "@nextui-org/react"; import { Switch } from "../core/Switch"; import { Button } from "../core/Button"; import { toast } from "react-toastify"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faLink } from "@fortawesome/free-solid-svg-icons"; export const TemplateSettings = () => { const { Loading Loading @@ -109,6 +113,21 @@ export const TemplateSettings = () => { > Show Mobile Tools </Switch> {url && ( <Button onPress={() => { const copied = `${window.location.origin}${window.location.pathname}#tu=${url}${ width != null ? `&tw=${width}` : "" }&tx=${x}&ty=${y}&ts=${style}&x=${x + (width || 0) / 2}&y=${y + (width || 0) / 2}&zoom=1`; navigator.clipboard.writeText(copied); toast.success("Link copied to clipboard!"); }} startContent={<FontAwesomeIcon icon={faLink} />} > Copy Shareable Link </Button> )} </section> )} </div> Loading