client/src/components/canvas.tsx aktualisiert
performance verbesserung
This commit is contained in:
@@ -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<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user