mihirmithani
Phase 2
25e7739
Raw History Blame
8.85 kB
'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<HTMLCanvasElement>(null)
const animationRef = useRef<number | null>(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<HTMLCanvasElement>) => {
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 (
<div className={cn('relative overflow-hidden bg-gray-100 dark:bg-gray-900 rounded-lg', className)}>
<canvas
ref={canvasRef}
width={width}
height={height}
className="block cursor-crosshair"
style={{ touchAction: 'none' }}
onClick={onInspectClick ? handleCanvasClick : undefined}
/>
</div>
)
}
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)
}