"use client"; import React, { useEffect, useRef, useState, useCallback } from "react"; import { cn } from "@/lib/utils"; type Point = { x: number; y: number }; const WhiteboardCanvas = () => { const containerRef = useRef(null); const canvasRef = useRef(null); const ctxRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [lastPoint, setLastPoint] = useState(null); const [strokeColor, setStrokeColor] = useState("#111111"); const [strokeWidth, setStrokeWidth] = useState(3); // Setup canvas with proper DPR scaling const resizeCanvas = useCallback(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const dpr = window.devicePixelRatio || 1; const rect = container.getBoundingClientRect(); canvas.width = Math.max(320, Math.floor(rect.width * dpr)); canvas.height = Math.max(320, Math.floor((rect.height - 60) * dpr)); // minus controls height canvas.style.width = `${Math.floor(canvas.width / dpr)}px`; canvas.style.height = `${Math.floor(canvas.height / dpr)}px`; const ctx = canvas.getContext("2d"); if (!ctx) return; ctxRef.current = ctx; // When resizing, keep existing content by redrawing the bitmap: // Create a temp bitmap from existing content before resizing (if any). // Note: For simplicity, we won't preserve content across resizes in this minimal version. // Initialize canvas background as transparent. Users can export PNG. ctx.lineCap = "round"; ctx.lineJoin = "round"; }, []); useEffect(() => { resizeCanvas(); const onResize = () => resizeCanvas(); window.addEventListener("resize", onResize); // If container changes size (e.g., sheet opening), observe it const ro = new ResizeObserver(() => resizeCanvas()); if (containerRef.current) { ro.observe(containerRef.current); } return () => { window.removeEventListener("resize", onResize); ro.disconnect(); }; }, [resizeCanvas]); const getRelativePoint = (e: PointerEvent | React.PointerEvent): Point => { const canvas = canvasRef.current!; const rect = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const x = (e.clientX - rect.left) * dpr; const y = (e.clientY - rect.top) * dpr; return { x, y }; }; const handlePointerDown = (e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); setIsDrawing(true); const p = getRelativePoint(e); setLastPoint(p); }; const handlePointerMove = (e: React.PointerEvent) => { if (!isDrawing || !lastPoint || !ctxRef.current) return; const p = getRelativePoint(e); const ctx = ctxRef.current; ctx.strokeStyle = strokeColor; const dpr = window.devicePixelRatio || 1; ctx.lineWidth = Math.max(1, strokeWidth * dpr); ctx.beginPath(); ctx.moveTo(lastPoint.x, lastPoint.y); ctx.lineTo(p.x, p.y); ctx.stroke(); setLastPoint(p); }; const handlePointerUp = (e: React.PointerEvent) => { if (e.currentTarget.hasPointerCapture(e.pointerId)) { e.currentTarget.releasePointerCapture(e.pointerId); } setIsDrawing(false); setLastPoint(null); }; const handlePointerLeave = () => { setIsDrawing(false); setLastPoint(null); }; const handleClear = () => { const canvas = canvasRef.current; const ctx = ctxRef.current; if (!canvas || !ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); }; const handleDownload = () => { const canvas = canvasRef.current; if (!canvas) return; const link = document.createElement("a"); link.download = "whiteboard.png"; link.href = canvas.toDataURL("image/png"); link.click(); }; return (
setStrokeColor(e.target.value)} className="h-8 w-10 rounded border border-black/10 dark:border-white/10 bg-transparent" /> setStrokeWidth(Number(e.target.value))} className="w-32 accent-[color:var(--accent)]" />
); }; export default WhiteboardCanvas;