diff --git a/client/src/components/optimized-canvas.tsx b/client/src/components/optimized-canvas.tsx index 1c62e2f..c4e7c2b 100644 --- a/client/src/components/optimized-canvas.tsx +++ b/client/src/components/optimized-canvas.tsx @@ -1,3 +1,4 @@ + import { useEffect, useRef, useState, useCallback } from "react"; import { Pixel } from "@shared/schema"; import { cn } from "@/lib/utils"; @@ -22,9 +23,11 @@ export function OptimizedCanvas({ const canvasRef = useRef(null); const containerRef = useRef(null); const [zoom, setZoom] = useState(1); - const [pixelSize, setPixelSize] = useState(8); + const pixelSize = Math.max(2, 8 * zoom); const [mouseCoords, setMouseCoords] = useState<{x: number, y: number} | null>(null); const [previewPixel, setPreviewPixel] = useState<{x: number, y: number} | null>(null); + const [isPanning, setIsPanning] = useState(false); + const [lastPanPosition, setLastPanPosition] = useState<{x: number, y: number} | null>(null); // Create pixel map for O(1) lookup const pixelMap = new Map(); @@ -117,10 +120,6 @@ export function OptimizedCanvas({ drawCanvas(); }, [drawCanvas]); - useEffect(() => { - setPixelSize(Math.max(2, 8 * zoom)); - }, [zoom]); - const getPixelCoordinates = (event: React.MouseEvent) => { const canvas = canvasRef.current; if (!canvas) return null; @@ -158,18 +157,114 @@ export function OptimizedCanvas({ const handleCanvasMouseLeave = () => { setMouseCoords(null); setPreviewPixel(null); + setIsPanning(false); + setLastPanPosition(null); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + // Mittlere Maustaste (Button 1) + if (e.button === 1) { + e.preventDefault(); + setIsPanning(true); + setLastPanPosition({ x: e.clientX, y: e.clientY }); + } + }; + + const handleMouseUp = (e: React.MouseEvent) => { + if (e.button === 1) { + e.preventDefault(); + setIsPanning(false); + setLastPanPosition(null); + } + }; + + const handleMouseMoveContainer = (e: React.MouseEvent) => { + if (isPanning && lastPanPosition && containerRef.current) { + e.preventDefault(); + + const deltaX = e.clientX - lastPanPosition.x; + const deltaY = e.clientY - lastPanPosition.y; + + const container = containerRef.current; + container.scrollLeft -= deltaX; + container.scrollTop -= deltaY; + + setLastPanPosition({ x: e.clientX, y: e.clientY }); + } + }; + + const zoomToPoint = (newZoom: number, mouseX?: number, mouseY?: number) => { + const container = containerRef.current; + if (!container) return; + + const clampedZoom = Math.max(0.1, Math.min(newZoom, 5)); + if (Math.abs(clampedZoom - zoom) < 0.01) return; + + const oldPixelSize = pixelSize; + const newPixelSize = Math.max(2, 8 * clampedZoom); + + // Aktuelle Scroll-Position + const currentScrollLeft = container.scrollLeft; + const currentScrollTop = container.scrollTop; + + let zoomPointX, zoomPointY; + + if (mouseX !== undefined && mouseY !== undefined) { + // Zoomen an Mausposition + const containerRect = container.getBoundingClientRect(); + const relativeMouseX = mouseX - containerRect.left; + const relativeMouseY = mouseY - containerRect.top; + + // Canvas-Koordinaten der Mausposition + zoomPointX = (currentScrollLeft + relativeMouseX - 32) / oldPixelSize; + zoomPointY = (currentScrollTop + relativeMouseY - 32) / oldPixelSize; + + setZoom(clampedZoom); + + // Neue Scroll-Position berechnen, damit Mausposition gleich bleibt + requestAnimationFrame(() => { + const newScrollLeft = zoomPointX * newPixelSize + 32 - relativeMouseX; + const newScrollTop = zoomPointY * newPixelSize + 32 - relativeMouseY; + + container.scrollTo({ + left: Math.max(0, Math.min(newScrollLeft, container.scrollWidth - container.clientWidth)), + top: Math.max(0, Math.min(newScrollTop, container.scrollHeight - container.clientHeight)), + behavior: 'auto' + }); + }); + } else { + // Zoomen im Center des Viewports + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + + zoomPointX = (currentScrollLeft + containerWidth / 2 - 32) / oldPixelSize; + zoomPointY = (currentScrollTop + containerHeight / 2 - 32) / oldPixelSize; + + setZoom(clampedZoom); + + requestAnimationFrame(() => { + const newScrollLeft = zoomPointX * newPixelSize + 32 - containerWidth / 2; + const newScrollTop = zoomPointY * newPixelSize + 32 - containerHeight / 2; + + container.scrollTo({ + left: Math.max(0, Math.min(newScrollLeft, container.scrollWidth - container.clientWidth)), + top: Math.max(0, Math.min(newScrollTop, container.scrollHeight - container.clientHeight)), + behavior: 'smooth' + }); + }); + } }; const handleZoomIn = () => { - setZoom(prev => Math.min(prev * 1.2, 3)); + zoomToPoint(zoom * 1.2); }; const handleZoomOut = () => { - setZoom(prev => Math.max(prev / 1.2, 0.5)); + zoomToPoint(zoom / 1.2); }; const handleResetZoom = () => { - setZoom(1); + zoomToPoint(1); }; const handleWheel = (e: React.WheelEvent) => { @@ -178,23 +273,39 @@ export function OptimizedCanvas({ const zoomFactor = 1.1; const delta = e.deltaY; + let newZoom; if (delta < 0) { - setZoom(prev => Math.min(prev * zoomFactor, 3)); + newZoom = zoom * zoomFactor; } else { - setZoom(prev => Math.max(prev / zoomFactor, 0.5)); + newZoom = zoom / zoomFactor; } + + zoomToPoint(newZoom, e.clientX, e.clientY); }; return (
{ + setIsPanning(false); + setLastPanPosition(null); + }} data-testid="canvas-container" + style={{ + scrollBehavior: isPanning ? 'auto' : 'auto' + }} > {/* Coordinate System Container */} -
+
{/* Top X-axis coordinates */}
{Array.from({ length: Math.ceil(canvasWidth / 10) }, (_, i) => ( @@ -203,7 +314,7 @@ export function OptimizedCanvas({ className="text-xs text-gray-400 text-center" style={{ width: `${10 * pixelSize}px`, - fontSize: `${Math.max(8, pixelSize * 0.8)}px` + fontSize: `${Math.max(8, Math.min(12, pixelSize * 0.8))}px` }} > {i * 10} @@ -222,7 +333,7 @@ export function OptimizedCanvas({ style={{ height: `${10 * pixelSize}px`, width: '24px', - fontSize: `${Math.max(8, pixelSize * 0.8)}px` + fontSize: `${Math.max(8, Math.min(12, pixelSize * 0.8))}px` }} > {i * 10} @@ -234,12 +345,13 @@ export function OptimizedCanvas({ e.preventDefault()} // Verhindert Rechtsklick-Menü data-testid="pixel-canvas" />
@@ -247,49 +359,64 @@ export function OptimizedCanvas({
{/* Zoom Controls */} -
+
+
+ {Math.round(zoom * 100)}% +
{/* Info Display */} -
-
- Canvas: {canvasWidth}x{canvasHeight} +
+
+ Canvas: {canvasWidth}×{canvasHeight}
-
- Zoom: {Math.round(zoom * 100)}% +
+ Pixel: {pixelSize}px | Zoom: {Math.round(zoom * 100)}%
{mouseCoords && ( -
- Mouse: ({mouseCoords.x}, {mouseCoords.y}) +
+ Position: ({mouseCoords.x}, {mouseCoords.y})
)} {previewPixel && !cooldownActive && ( -
+
Vorschau: {selectedColor}
)} + {cooldownActive && ( +
+ Cooldown aktiv +
+ )} + {isPanning && ( +
+ Bewege Canvas (Mittlere Maustaste) +
+ )}
); -} \ No newline at end of file +}