- 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>
197 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|