client/src/components/canvas.tsx aktualisiert

performance verbesserung
This commit is contained in:
2025-08-18 19:17:32 +02:00
parent 836d14fbef
commit b99dc790b9

View File

@@ -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,
onPixelClick,
cooldownActive
}: CanvasProps) {
}: OptimizedCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(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<HTMLCanvasElement>) => {
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<HTMLCanvasElement>) => {
if (cooldownActive) return;
const coords = getPixelCoordinates(event);
if (coords) {
onPixelClick(coords.x, coords.y);
}
};
const handlePixelMouseLeave = () => {
const handleCanvasMouseMove = (event: React.MouseEvent<HTMLCanvasElement>) => {
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 (
<div className="flex-1 relative bg-canvas-bg overflow-hidden">
<div
@@ -133,52 +231,50 @@ export function Canvas({
</div>
{/* Main Canvas */}
<div
className="border border-gray-400 relative"
style={canvasStyle}
<canvas
ref={canvasRef}
className={cn(
"border border-gray-400 cursor-pointer",
cooldownActive && "cursor-not-allowed"
)}
onClick={handleCanvasClick}
onMouseMove={handleCanvasMouseMove}
onMouseLeave={handleCanvasMouseLeave}
data-testid="pixel-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 (
<div
key={`${x}-${y}`}
className={cn(
"pixel cursor-pointer hover:scale-110 hover:z-10 absolute",
cooldownActive && "cursor-not-allowed",
isPreview && !cooldownActive && "pixel-preview"
)}
style={{
backgroundColor: previewColor,
width: `${pixelSize}px`,
height: `${pixelSize}px`,
left: `${x * pixelSize}px`,
top: `${y * pixelSize}px`,
opacity: isPreview && !cooldownActive ? 0.7 : 1
}}
onClick={() => handlePixelClick(x, y)}
onMouseEnter={() => handlePixelMouseEnter(x, y)}
onMouseLeave={handlePixelMouseLeave}
data-testid={`pixel-${x}-${y}`}
data-x={x}
data-y={y}
/>
);
})
)}
</div>
/>
</div>
</div>
</div>
{/* Coordinate Info Display */}
<div className="absolute top-4 left-4 bg-panel-bg px-3 py-2 rounded-lg border border-gray-600">
<div className="text-sm text-gray-300">
Canvas: {canvasWidth} × {canvasHeight}
{/* Zoom Controls */}
<div className="absolute top-4 right-4 flex flex-col gap-2 bg-white/80 p-2 rounded shadow">
<button
onClick={handleZoomIn}
className="px-2 py-1 bg-blue-500 text-white rounded text-sm hover:bg-blue-600"
data-testid="button-zoom-in"
>
+
</button>
<button
onClick={handleZoomOut}
className="px-2 py-1 bg-blue-500 text-white rounded text-sm hover:bg-blue-600"
data-testid="button-zoom-out"
>
-
</button>
<button
onClick={handleResetZoom}
className="px-2 py-1 bg-gray-500 text-white rounded text-xs hover:bg-gray-600"
data-testid="button-zoom-reset"
>
100%
</button>
</div>
{/* Info Display */}
<div className="absolute bottom-4 left-4 bg-white/80 p-3 rounded shadow text-sm">
<div className="text-xs text-gray-600">
Canvas: {canvasWidth}x{canvasHeight}
</div>
<div className="text-xs text-gray-400">
Zoom: {Math.round(zoom * 100)}%
@@ -194,42 +290,6 @@ export function Canvas({
</div>
)}
</div>
{/* Zoom Controls */}
<div className="absolute bottom-4 right-4 flex flex-col space-y-2">
<button
onClick={handleZoomIn}
className="w-10 h-10 bg-panel-bg hover:bg-panel-hover rounded-lg flex items-center justify-center transition-colors"
data-testid="button-zoom-in"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</button>
<button
onClick={handleZoomOut}
className="w-10 h-10 bg-panel-bg hover:bg-panel-hover rounded-lg flex items-center justify-center transition-colors"
data-testid="button-zoom-out"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M18 12H6"/>
</svg>
</button>
<button
onClick={handleResetZoom}
className="w-10 h-10 bg-panel-bg hover:bg-panel-hover rounded-lg flex items-center justify-center transition-colors"
data-testid="button-reset-zoom"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</button>
</div>
{/* Cooldown Overlay */}
{cooldownActive && (
<div className="fixed inset-0 cooldown-overlay pointer-events-none opacity-30 z-40" />
)}
</div>
);
}