Download src/components/canvas/ConvolutionCanvas.tsx from TechAvenger/kernel-convolution-visualizer: direct link, hf CLI and curl.
- Browser
- Download file 8.85 kB
-
https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/ConvolutionCanvas.tsx
- Command line
-
hf download hf://spaces/TechAvenger/kernel-convolution-visualizer@25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/ConvolutionCanvas.tsx
-
curl -L -o ConvolutionCanvas.tsx https://huggingface.co/spaces/TechAvenger/kernel-convolution-visualizer/resolve/25e773913dbd4e9dcd6b40f787d68b280ccdfca1/src/components/canvas/ConvolutionCanvas.tsx
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) | |
| } |