diff --git a/client/src/components/canvas.tsx b/client/src/components/canvas.tsx index d12b766..1c62e2f 100644 --- a/client/src/components/canvas.tsx +++ b/client/src/components/canvas.tsx @@ -1,26 +1,25 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useCallback } from "react"; import { Pixel } from "@shared/schema"; import { cn } from "@/lib/utils"; -interface CanvasProps { +interface OptimizedCanvasProps { pixels: Pixel[]; selectedColor: string; canvasWidth: number; canvasHeight: number; - onPixelClick: (x: number, y: number) => void; cooldownActive: boolean; } -export function Canvas({ +export function OptimizedCanvas({ pixels, selectedColor, canvasWidth, - canvasHeight, - + canvasHeight, onPixelClick, cooldownActive -}: CanvasProps) { +}: OptimizedCanvasProps) { + const canvasRef = useRef(null); const containerRef = useRef(null); const [zoom, setZoom] = useState(1); const [pixelSize, setPixelSize] = useState(8); @@ -33,19 +32,130 @@ export function Canvas({ pixelMap.set(`${pixel.x},${pixel.y}`, pixel.color); }); - const handlePixelClick = (x: number, y: number) => { - if (cooldownActive) return; - onPixelClick(x, y); + const drawCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Set canvas size + const newWidth = canvasWidth * pixelSize; + const newHeight = canvasHeight * pixelSize; + + if (canvas.width !== newWidth || canvas.height !== newHeight) { + canvas.width = newWidth; + canvas.height = newHeight; + } + + // Clear canvas mit besserer Performance + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Hintergrund setzen + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Disable smoothing für pixelgenaue Darstellung + ctx.imageSmoothingEnabled = false; + + // Draw pixels - nur gesetzte Pixel zeichnen + pixels.forEach(pixel => { + ctx.fillStyle = pixel.color; + ctx.fillRect(pixel.x * pixelSize, pixel.y * pixelSize, pixelSize, pixelSize); + }); + + // Grid nur bei größerem Zoom + if (pixelSize > 6) { + ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.lineWidth = 1; + + // Nur sichtbare Grid-Linien zeichnen + const step = Math.max(1, Math.floor(10 / pixelSize)); + + // Vertical lines + for (let x = 0; x <= canvasWidth; x += step) { + ctx.beginPath(); + ctx.moveTo(x * pixelSize, 0); + ctx.lineTo(x * pixelSize, canvas.height); + ctx.stroke(); + } + + // Horizontal lines + for (let y = 0; y <= canvasHeight; y += step) { + ctx.beginPath(); + ctx.moveTo(0, y * pixelSize); + ctx.lineTo(canvas.width, y * pixelSize); + ctx.stroke(); + } + } + + // Draw preview pixel + if (previewPixel && !cooldownActive) { + ctx.fillStyle = selectedColor; + ctx.globalAlpha = 0.7; + ctx.fillRect( + previewPixel.x * pixelSize, + previewPixel.y * pixelSize, + pixelSize, + pixelSize + ); + + // Preview border + ctx.globalAlpha = 1; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.lineWidth = 2; + ctx.strokeRect( + previewPixel.x * pixelSize, + previewPixel.y * pixelSize, + pixelSize, + pixelSize + ); + } + }, [canvasWidth, canvasHeight, pixelSize, pixels, previewPixel, selectedColor, cooldownActive]); + + useEffect(() => { + drawCanvas(); + }, [drawCanvas]); + + useEffect(() => { + setPixelSize(Math.max(2, 8 * zoom)); + }, [zoom]); + + const getPixelCoordinates = (event: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return null; + + const rect = canvas.getBoundingClientRect(); + const x = Math.floor((event.clientX - rect.left) / pixelSize); + const y = Math.floor((event.clientY - rect.top) / pixelSize); + + if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { + return { x, y }; + } + return null; }; - const handlePixelMouseEnter = (x: number, y: number) => { - setMouseCoords({ x, y }); - if (!cooldownActive) { - setPreviewPixel({ x, y }); + const handleCanvasClick = (event: React.MouseEvent) => { + if (cooldownActive) return; + + const coords = getPixelCoordinates(event); + if (coords) { + onPixelClick(coords.x, coords.y); } }; - const handlePixelMouseLeave = () => { + const handleCanvasMouseMove = (event: React.MouseEvent) => { + const coords = getPixelCoordinates(event); + setMouseCoords(coords); + + if (coords && !cooldownActive) { + setPreviewPixel(coords); + } else { + setPreviewPixel(null); + } + }; + + const handleCanvasMouseLeave = () => { setMouseCoords(null); setPreviewPixel(null); }; @@ -69,24 +179,12 @@ export function Canvas({ const delta = e.deltaY; if (delta < 0) { - // Rein zoomen setZoom(prev => Math.min(prev * zoomFactor, 3)); } else { - // Raus zoomen setZoom(prev => Math.max(prev / zoomFactor, 0.5)); } }; - useEffect(() => { - setPixelSize(Math.max(2, 8 * zoom)); - }, [zoom]); - - const canvasStyle = { - width: `${canvasWidth * pixelSize}px`, - height: `${canvasHeight * pixelSize}px`, - position: 'relative' as const, - }; - return (
{/* Main Canvas */} -
- {Array.from({ length: canvasHeight }, (_, y) => - Array.from({ length: canvasWidth }, (_, x) => { - const pixelColor = pixelMap.get(`${x},${y}`) || "#FFFFFF"; - const isPreview = previewPixel && previewPixel.x === x && previewPixel.y === y; - const previewColor = isPreview && !cooldownActive ? selectedColor : pixelColor; - - return ( -
handlePixelClick(x, y)} - onMouseEnter={() => handlePixelMouseEnter(x, y)} - onMouseLeave={handlePixelMouseLeave} - data-testid={`pixel-${x}-${y}`} - data-x={x} - data-y={y} - /> - ); - }) - )} -
+ />
- {/* Coordinate Info Display */} -
-
- Canvas: {canvasWidth} × {canvasHeight} + {/* Zoom Controls */} +
+ + + +
+ + {/* Info Display */} +
+
+ Canvas: {canvasWidth}x{canvasHeight}
Zoom: {Math.round(zoom * 100)}% @@ -194,42 +290,6 @@ export function Canvas({
)}
- - {/* Zoom Controls */} -
- - - -
- - {/* Cooldown Overlay */} - {cooldownActive && ( -
- )}
); -} +} \ No newline at end of file