Files
paint-ui/components/canvas/mini-map.tsx
Sebastian Krüger 6a47efc164 fix: resolve TypeScript errors in mini-map and layer-effects-panel
- Add null check for layer.canvas before drawing in mini-map
- Change JSX.Element to React.ReactElement in layer-effects-panel
- Fixes deployment build errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 17:53:59 +01:00

197 lines
6.1 KiB
TypeScript

'use client';
import { useRef, useEffect, useState, useCallback } from 'react';
import { useCanvasStore, useLayerStore } from '@/store';
import { cn } from '@/lib/utils';
import { Maximize2, Minimize2 } from 'lucide-react';
const MINIMAP_MAX_SIZE = 200; // Maximum width/height in pixels
export function MiniMap() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isExpanded, setIsExpanded] = useState(true);
const [isDragging, setIsDragging] = useState(false);
const { width: canvasWidth, height: canvasHeight, zoom, offsetX, offsetY, setZoom, setPanOffset } = useCanvasStore();
const { layers } = useLayerStore();
// Calculate minimap dimensions maintaining aspect ratio
const aspectRatio = canvasWidth / canvasHeight;
let minimapWidth = MINIMAP_MAX_SIZE;
let minimapHeight = MINIMAP_MAX_SIZE;
if (aspectRatio > 1) {
// Landscape
minimapHeight = MINIMAP_MAX_SIZE / aspectRatio;
} else {
// Portrait
minimapWidth = MINIMAP_MAX_SIZE * aspectRatio;
}
const scale = minimapWidth / canvasWidth;
// Render minimap
useEffect(() => {
if (!canvasRef.current || !isExpanded) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
canvas.width = minimapWidth;
canvas.height = minimapHeight;
// Clear canvas
ctx.clearRect(0, 0, minimapWidth, minimapHeight);
// Draw checkerboard background
const checkSize = 4;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, minimapWidth, minimapHeight);
ctx.fillStyle = '#e0e0e0';
for (let y = 0; y < minimapHeight; y += checkSize) {
for (let x = 0; x < minimapWidth; x += checkSize) {
if ((x / checkSize + y / checkSize) % 2 === 0) {
ctx.fillRect(x, y, checkSize, checkSize);
}
}
}
// Draw all visible layers (scaled down)
ctx.save();
ctx.scale(scale, scale);
layers
.filter((layer) => layer.visible && layer.canvas)
.sort((a, b) => a.order - b.order) // Bottom to top
.forEach((layer) => {
if (!layer.canvas) return;
ctx.globalAlpha = layer.opacity;
ctx.drawImage(layer.canvas, layer.x, layer.y);
});
ctx.restore();
// Draw viewport indicator
const viewportWidth = window.innerWidth - 344; // Approximate viewport width
const viewportHeight = window.innerHeight - 138; // Approximate viewport height
const visibleCanvasWidth = viewportWidth / zoom;
const visibleCanvasHeight = viewportHeight / zoom;
const viewportX = (-offsetX / zoom) * scale;
const viewportY = (-offsetY / zoom) * scale;
const viewportW = visibleCanvasWidth * scale;
const viewportH = visibleCanvasHeight * scale;
// Draw viewport rectangle
ctx.strokeStyle = '#3b82f6'; // Primary blue
ctx.lineWidth = 2;
ctx.strokeRect(viewportX, viewportY, viewportW, viewportH);
// Draw semi-transparent fill
ctx.fillStyle = 'rgba(59, 130, 246, 0.1)';
ctx.fillRect(viewportX, viewportY, viewportW, viewportH);
}, [layers, zoom, offsetX, offsetY, minimapWidth, minimapHeight, scale, isExpanded]);
// Handle click/drag to change viewport
const handlePointerDown = useCallback((e: React.PointerEvent) => {
if (!canvasRef.current) return;
setIsDragging(true);
const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Convert minimap coordinates to canvas coordinates
const canvasX = x / scale;
const canvasY = y / scale;
// Center the viewport on the clicked point
const newOffsetX = -canvasX * zoom + (window.innerWidth - 344) / 2;
const newOffsetY = -canvasY * zoom + (window.innerHeight - 138) / 2;
setPanOffset(newOffsetX, newOffsetY);
}, [scale, zoom, setPanOffset]);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (!isDragging || !canvasRef.current) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const canvasX = x / scale;
const canvasY = y / scale;
const newOffsetX = -canvasX * zoom + (window.innerWidth - 344) / 2;
const newOffsetY = -canvasY * zoom + (window.innerHeight - 138) / 2;
setPanOffset(newOffsetX, newOffsetY);
}, [isDragging, scale, zoom, setPanOffset]);
const handlePointerUp = useCallback(() => {
setIsDragging(false);
}, []);
if (!isExpanded) {
return (
<div
ref={containerRef}
className="absolute bottom-4 right-4 z-10 bg-card border border-border rounded-md shadow-lg"
>
<button
onClick={() => setIsExpanded(true)}
className="p-2 hover:bg-accent rounded-md transition-colors"
title="Show Navigator"
>
<Maximize2 className="h-4 w-4" />
</button>
</div>
);
}
return (
<div
ref={containerRef}
className="absolute bottom-4 right-4 z-10 bg-card border border-border rounded-md shadow-lg p-2 flex flex-col gap-2"
>
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">Navigator</span>
<button
onClick={() => setIsExpanded(false)}
className="p-1 hover:bg-accent rounded transition-colors"
title="Hide Navigator"
>
<Minimize2 className="h-3 w-3" />
</button>
</div>
{/* Mini-map canvas */}
<canvas
ref={canvasRef}
className={cn(
'border border-border rounded cursor-pointer',
isDragging && 'cursor-grabbing'
)}
style={{
width: `${minimapWidth}px`,
height: `${minimapHeight}px`,
}}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
/>
{/* Zoom indicator */}
<div className="text-xs text-center text-muted-foreground font-mono">
{Math.round(zoom * 100)}%
</div>
</div>
);
}