'use client' import { useRef, useEffect, useCallback, useMemo } from 'react' import { useAppStore, useConvolutionStore, useSettingsStore } from '@/store' import type { ConvolutionStep } from '@/types' import { cn } from '@/utils/helpers' interface ConvolutionCanvasProps { width?: number height?: number className?: string onInspectClick?: (x: number, y: number) => void } export function ConvolutionCanvas({ width = 800, height = 600, className, onInspectClick }: ConvolutionCanvasProps) { const canvasRef = useRef(null) const animationRef = useRef(null) const { originalImageData, processedImageData, convolution } = useAppStore() const { showOverlay, zoomLevel, panOffset, padding } = useSettingsStore() const currentStep = convolution.currentStep const drawCanvas = useCallback(() => { const canvas = canvasRef.current const ctx = canvas?.getContext('2d', { willReadFrequently: true }) if (!canvas || !ctx) return const displayWidth = Math.round(width * zoomLevel) const displayHeight = Math.round(height * zoomLevel) canvas.width = displayWidth canvas.height = displayHeight ctx.clearRect(0, 0, displayWidth, displayHeight) ctx.imageSmoothingEnabled = false // Draw original image as background (dimmed) if (originalImageData) { const tempCanvas = document.createElement('canvas') tempCanvas.width = originalImageData.width tempCanvas.height = originalImageData.height const tempCtx = tempCanvas.getContext('2d') if (tempCtx) { tempCtx.putImageData(originalImageData, 0, 0) ctx.globalAlpha = 0.3 ctx.drawImage( tempCanvas, 0, 0, originalImageData.width, originalImageData.height, panOffset.x, panOffset.y, originalImageData.width * zoomLevel, originalImageData.height * zoomLevel ) ctx.globalAlpha = 1 } } // Draw processed image progressively if (processedImageData) { const tempCanvas = document.createElement('canvas') tempCanvas.width = processedImageData.width tempCanvas.height = processedImageData.height const tempCtx = tempCanvas.getContext('2d') if (tempCtx) { tempCtx.putImageData(processedImageData, 0, 0) ctx.drawImage( tempCanvas, 0, 0, processedImageData.width, processedImageData.height, panOffset.x, panOffset.y, processedImageData.width * zoomLevel, processedImageData.height * zoomLevel ) } } // Draw convolution visualization overlays if (currentStep && currentStep.x >= 0 && currentStep.y >= 0) { drawConvolutionOverlay(ctx, currentStep, zoomLevel, panOffset, showOverlay) } // Draw progress indicator if (convolution.status === 'running' || convolution.status === 'paused') { drawProgressBar(ctx, displayWidth, displayHeight, convolution.progress) } }, [ originalImageData, processedImageData, currentStep, convolution.status, convolution.progress, zoomLevel, panOffset, showOverlay, width, height, ]) const handleCanvasClick = useCallback( (e: React.MouseEvent) => { if (!onInspectClick) return const rect = e.currentTarget.getBoundingClientRect() const canvasX = e.clientX - rect.left const canvasY = e.clientY - rect.top // Convert canvas coordinates to image coordinates const imageX = Math.floor((canvasX - panOffset.x) / zoomLevel) const imageY = Math.floor((canvasY - panOffset.y) / zoomLevel) if (imageX >= 0 && imageX < width && imageY >= 0 && imageY < height) { onInspectClick(imageX, imageY) } }, [onInspectClick, panOffset, zoomLevel, width, height] ) useEffect(() => { drawCanvas() }, [drawCanvas]) // Animation loop for smooth rendering useEffect(() => { const animate = () => { drawCanvas() animationRef.current = requestAnimationFrame(animate) } animationRef.current = requestAnimationFrame(animate) return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current) } }, [drawCanvas]) return (
) } function drawConvolutionOverlay( ctx: CanvasRenderingContext2D, step: ConvolutionStep, zoom: number, pan: { x: number; y: number }, showOverlay: { kernelBoundary: boolean pixelValues: boolean multiplications: boolean summation: boolean } ) { const { x, y, inputPatch, kernel, multiplications, sum, normalizedValue, channel } = step const kernelSize = kernel.length const halfKernel = Math.floor(kernelSize / 2) const cellSize = Math.max(20, Math.min(60, 40 * zoom)) const centerX = pan.x + x * zoom const centerY = pan.y + y * zoom // Draw kernel boundary if (showOverlay.kernelBoundary) { ctx.strokeStyle = 'rgba(59, 130, 246, 0.8)' ctx.lineWidth = 2 ctx.setLineDash([5, 5]) ctx.strokeRect( centerX - halfKernel * cellSize * zoom, centerY - halfKernel * cellSize * zoom, kernelSize * cellSize * zoom, kernelSize * cellSize * zoom ) ctx.setLineDash([]) } // Draw kernel matrix with values if (showOverlay.pixelValues || showOverlay.multiplications) { const fontSize = Math.max(10, Math.min(14, 12 * zoom)) ctx.font = `${fontSize}px monospace` ctx.textAlign = 'center' ctx.textBaseline = 'middle' for (let ky = 0; ky < kernelSize; ky++) { for (let kx = 0; kx < kernelSize; kx++) { const cellX = centerX + (kx - halfKernel) * cellSize * zoom const cellY = centerY + (ky - halfKernel) * cellSize * zoom // Cell background ctx.fillStyle = 'rgba(255, 255, 255, 0.9)' ctx.fillRect(cellX - cellSize / 2, cellY - cellSize / 2, cellSize, cellSize) ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)' ctx.strokeRect(cellX - cellSize / 2, cellY - cellSize / 2, cellSize, cellSize) if (showOverlay.multiplications && multiplications[ky]?.[kx] !== undefined) { // Show multiplication: pixel × kernel const pixelVal = inputPatch[ky]?.[kx] ?? 0 const kernelVal = kernel[ky]?.[kx] ?? 0 const mult = multiplications[ky][kx] ctx.fillStyle = '#1e40af' ctx.fillText(`${pixelVal.toFixed(2)}×${kernelVal.toFixed(2)}`, cellX, cellY - fontSize / 2) ctx.fillStyle = '#dc2626' ctx.fillText(`=${mult.toFixed(3)}`, cellX, cellY + fontSize / 2) } else if (showOverlay.pixelValues) { // Show kernel value const kernelVal = kernel[ky]?.[kx] ?? 0 ctx.fillStyle = '#1e40af' ctx.fillText(kernelVal.toFixed(2), cellX, cellY) } } } } // Draw summation display if (showOverlay.summation) { const fontSize = Math.max(12, Math.min(16, 14 * zoom)) ctx.font = `bold ${fontSize}px monospace` ctx.textAlign = 'left' ctx.textBaseline = 'top' const sumX = centerX + (halfKernel + 1) * cellSize * zoom const sumY = centerY - halfKernel * cellSize * zoom // Background const padding = 8 const textWidth = ctx.measureText(`Σ = ${sum.toFixed(3)}`).width + padding * 2 const textHeight = fontSize * 3 + padding * 2 ctx.fillStyle = 'rgba(255, 255, 255, 0.95)' ctx.fillRect(sumX - padding, sumY - padding, textWidth, textHeight) ctx.strokeStyle = '#3b82f6' ctx.lineWidth = 2 ctx.strokeRect(sumX - padding, sumY - padding, textWidth, textHeight) ctx.fillStyle = '#1e40af' ctx.fillText(`Channel: ${['R', 'G', 'B', 'A'][channel] ?? channel}`, sumX, sumY) ctx.fillText(`Sum: ${sum.toFixed(3)}`, sumX, sumY + fontSize + 4) ctx.fillStyle = '#16a34a' ctx.fillText(`Output: ${normalizedValue.toFixed(3)}`, sumX, sumY + (fontSize + 4) * 2) } } function drawProgressBar( ctx: CanvasRenderingContext2D, width: number, height: number, progress: number ) { const barHeight = 4 const barWidth = width * 0.8 const x = (width - barWidth) / 2 const y = height - 20 ctx.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx.fillRect(x, y, barWidth, barHeight) ctx.fillStyle = '#3b82f6' ctx.fillRect(x, y, barWidth * progress, barHeight) ctx.fillStyle = '#fff' ctx.font = '12px monospace' ctx.textAlign = 'center' ctx.fillText(`${Math.round(progress * 100)}%`, width / 2, y - 4) }