"use client"; import { useEffect, useRef, useState } from "react"; import { getSocket } from "../lib/socket-client"; import { useGame } from "../lib/store"; const PALETTE = [ "#FF5C5C","#FFD23F","#4ECDC4","#A593E0","#5BCEFA","#FF8FB1","#1AAE56","#7B3F00", "#2D2D2D","#FFFFFF","#F87171","#FBBF24","#34D399","#60A5FA","#A78BFA","#F472B6" ]; export default function ColorGame() { const room = useGame((s) => s.room); const canvasRef = useRef(null); const stageRef = useRef(null); const drawingRef = useRef(false); const lastRef = useRef<{x:number;y:number}|null>(null); const [color, setColor] = useState("#FF5C5C"); const [size, setSize] = useState(8); const [tool, setTool] = useState<"brush"|"eraser"|"bucket">("brush"); const [regions, setRegions] = useState>({}); const useTemplate = room?.color?.canvasType === "template"; const templateId = room?.color?.templateId || "mandala"; useEffect(() => { const socket = getSocket(); const onState = (s: any) => { // redraw all strokes clearLocalCanvas(); for (const stroke of s.strokes || []) drawStrokeLocal(stroke); setRegions(s.regions || {}); }; const onStroke = (s: any) => drawStrokeLocal(s); const onBucket = (b: any) => setRegions((r) => ({ ...r, [b.regionId]: b.color })); socket.on("color:state", onState); socket.on("color:strokeBroadcast", onStroke); socket.on("color:bucketBroadcast", onBucket); return () => { socket.off("color:state", onState); socket.off("color:strokeBroadcast", onStroke); socket.off("color:bucketBroadcast", onBucket); }; }, []); useEffect(() => { const resize = () => { const canvas = canvasRef.current; const stage = stageRef.current; if (!canvas || !stage) return; const rect = stage.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = Math.max(100, rect.width * dpr); canvas.height = Math.max(100, rect.height * dpr); canvas.style.width = rect.width + "px"; canvas.style.height = rect.height + "px"; const ctx = canvas.getContext("2d"); if (ctx) ctx.scale(dpr, dpr); }; resize(); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []); const drawStrokeLocal = (s: any) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx || !s.points || s.points.length === 0) return; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = s.tool === "eraser" ? "#FFF8E7" : s.color; ctx.lineWidth = s.size; ctx.beginPath(); ctx.moveTo(s.points[0].x, s.points[0].y); for (const p of s.points.slice(1)) ctx.lineTo(p.x, p.y); ctx.stroke(); }; const clearLocalCanvas = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); }; const getPos = (e: React.PointerEvent) => { const canvas = canvasRef.current!; const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }; const pointBuf = useRef<{x:number;y:number}[]>([]); const onDown = (e: React.PointerEvent) => { if (tool === "bucket") return; e.preventDefault(); drawingRef.current = true; const p = getPos(e); lastRef.current = p; pointBuf.current = [p]; drawStrokeLocal({ points: [p, p], color, size, tool }); }; const onMove = (e: React.PointerEvent) => { if (!drawingRef.current) return; const p = getPos(e); drawStrokeLocal({ points: [lastRef.current!, p], color, size, tool }); pointBuf.current.push(p); lastRef.current = p; if (pointBuf.current.length >= 6) flushStroke(); }; const onUp = () => { if (!drawingRef.current) return; drawingRef.current = false; lastRef.current = null; flushStroke(); }; const flushStroke = () => { if (pointBuf.current.length < 2) { pointBuf.current = []; return; } const stroke = { points: pointBuf.current.slice(), color, size, tool }; pointBuf.current = pointBuf.current.slice(-1); getSocket().emit("color:stroke", stroke); }; const handleRegionClick = (regionId: string) => { if (tool !== "bucket") return; getSocket().emit("color:bucket", { regionId, color }); }; const reset = () => { if (!confirm("Clear the whole canvas?")) return; getSocket().emit("color:reset", {}); }; const TemplateSvg = useTemplate ? TEMPLATE_SVGS[templateId] || TEMPLATE_SVGS.mandala : null; return (
{/* Template SVG painted underneath; canvas above for freehand */} {TemplateSvg && ( )}
{PALETTE.map(c => (
{[ {id:"brush", label:"Brush"}, {id:"eraser", label:"Eraser"}, ...(useTemplate ? [{id:"bucket", label:"Bucket"}] : []), ].map((t: any) => ( ))}
{[4,8,16].map((s, i) => ( ))}
); } // Lightweight inline templates with named regions function MandalaT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return ( onRegionClick("bg")} /> onRegionClick("c1")}/> onRegionClick("c2")}/> onRegionClick("c3")}/> {[0,45,90,135,180,225,270,315].map((deg, i) => { const r = 100; const x = 200 + Math.cos(deg*Math.PI/180) * r; const y = 150 + Math.sin(deg*Math.PI/180) * r; return onRegionClick(`p${i}`)}/>; })} ); } function CatT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("body")}/> onRegionClick("head")}/> onRegionClick("ear1")}/> onRegionClick("ear2")}/> onRegionClick("nose")}/> ; } function TreeT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("trunk")}/> onRegionClick("leaves")}/> onRegionClick("leaves2")}/> onRegionClick("leaves3")}/> ; } function FishT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("body")}/> onRegionClick("tail")}/> ; } function ButterflyT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("body")}/> onRegionClick("wing1")}/> onRegionClick("wing2")}/> onRegionClick("wing3")}/> onRegionClick("wing4")}/> ; } function HouseT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("walls")}/> onRegionClick("roof")}/> onRegionClick("door")}/> onRegionClick("win1")}/> onRegionClick("win2")}/> ; } function FlowerT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("stem")}/> {[0,72,144,216,288].map((deg, i) => { const r = 50; const x = 200 + Math.cos((deg-90)*Math.PI/180) * r; const y = 140 + Math.sin((deg-90)*Math.PI/180) * r; return onRegionClick(`pet${i}`)} transform={`rotate(${deg-90} ${x} ${y})`}/>; })} onRegionClick("center")}/> ; } function SunT({ regions, onRegionClick }: any) { const get = (id: string, def: string) => regions[id] || def; return onRegionClick("bg")}/> onRegionClick("sun")}/> {[0,30,60,90,120,150,180,210,240,270,300,330].map((deg, i) => { const r1 = 75, r2 = 105; const x1 = 200 + Math.cos(deg*Math.PI/180)*r1; const y1 = 150 + Math.sin(deg*Math.PI/180)*r1; const x2 = 200 + Math.cos(deg*Math.PI/180)*r2; const y2 = 150 + Math.sin(deg*Math.PI/180)*r2; return ; })} ; } const TEMPLATE_SVGS: Record = { mandala: MandalaT, cat: CatT, tree: TreeT, fish: FishT, butterfly: ButterflyT, house: HouseT, flower: FlowerT, sun: SunT, };