'use client' import { useCallback, useMemo, useState } from 'react' import { useStaticConvolution } from '@/hooks/useStaticConvolution' import { useAppStore } from '@/store' import { Card, CardContent, Badge, Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' import { cn } from '@/utils/helpers' import { Calculator, Grid, Palette } from 'lucide-react' interface StaticConvolutionViewProps { className?: string inspectedPosition: { x: number; y: number } | null onClearInspection: () => void } export function StaticConvolutionView({ className, inspectedPosition, onClearInspection }: StaticConvolutionViewProps) { const { computeStep, getPaddedImagePreview, kernel, normalize, padding, imageWidth, imageHeight, hasImage, hasKernel, } = useStaticConvolution() const { settings } = useAppStore() const [activeChannel, setActiveChannel] = useState(0) const [activeTab, setActiveTab] = useState('overview') if (!hasImage || !hasKernel) { return (

Upload an image and select a kernel to inspect convolution steps

) } const step = inspectedPosition ? computeStep(inspectedPosition.x, inspectedPosition.y, activeChannel) : null const channelLabels = ['Red', 'Green', 'Blue', 'Alpha'] const channelColors = ['text-red-600', 'text-green-600', 'text-blue-600', 'text-gray-600'] // Get padded image preview for visualization const paddedPreview = inspectedPosition ? getPaddedImagePreview(kernel.length) : null const halfKernel = Math.floor(kernel.length / 2) const centerPx = halfKernel const centerPy = halfKernel return ( {/* Header */}

Static Convolution Inspector

{padding} padding {normalize && Normalized}
{/* Inspected Position Info */} {inspectedPosition && (
Position: ({inspectedPosition.x}, {inspectedPosition.y})
)} {/* Kernel Matrix Display */}

Kernel Matrix ({kernel.length}×{kernel.length})

{kernel.map((row, i) => row.map((val, j) => (
0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300 border-green-200' : val < 0 ? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-200' : 'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300 border-gray-200' )} > {val.toFixed(val === Math.round(val) ? 0 : 2)}
)) )}
{/* Step Details Tabs */} {step && ( Overview Image Patch Multiplications Summation Padding
Input Patch (Normalized 0-1)
{step.inputPatch.map((row, i) => row.map((val, j) => (
0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700' )} > {val.toFixed(3)}
)) )}
Kernel Values
{step.kernel.map((row, i) => row.map((val, j) => (
0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300' : val < 0 ? 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300' : 'bg-gray-50 text-gray-800 dark:bg-gray-800 dark:text-gray-300' )} > {val.toFixed(val === Math.round(val) ? 0 : 2)}
)) )}
Computation Summary
Raw Sum: {step.sum.toFixed(6)}
Normalized: {step.normalizedValue.toFixed(6)}
Output (0-255): {Math.round(step.normalizedValue * 255)}
Progress: {Math.round(step.progress * 100)}%
Image Patch Under Kernel at ({inspectedPosition?.x}, {inspectedPosition?.y}) - Channel: {channelLabels[activeChannel]}
{step.inputPatch.map((row, i) => row.map((val, j) => (
0.5 ? 'bg-blue-50 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 border-gray-200' )} >
{val.toFixed(3)}
{val.toFixed(3)}
)) )}
Element-wise Multiplication: Pixel × Kernel
{step.multiplications.map((row, i) => row.map((mult, j) => { const pixelVal = step.inputPatch[i]?.[j] ?? 0 const kernelVal = step.kernel[i]?.[j] ?? 0 return (
{pixelVal.toFixed(3)}
×
0 ? '#16a34a' : kernelVal < 0 ? '#dc2626' : '#6b7280' }}> {kernelVal.toFixed(2)}
=
{mult.toFixed(6)}
) }) )}
Summation: Σ(Pixel × Kernel)
{step.multiplications.map((row, i) => row.map((mult, j) => { const pixelVal = step.inputPatch[i]?.[j] ?? 0 const kernelVal = step.kernel[i]?.[j] ?? 0 return (
({pixelVal.toFixed(3)} × {kernelVal.toFixed(2)}) = {mult.toFixed(6)}
) }) )}
Sum: {step.sum.toFixed(6)}
After normalization (÷ {kernel.flat().reduce((a, b) => a + b, 0).toFixed(normalize ? 0 : 2)}): {step.normalizedValue.toFixed(6)}
Final Output (0-255): {Math.round(step.normalizedValue * 255)}
Padded Image Preview ({padding} padding) - Kernel centered at ({inspectedPosition?.x}, {inspectedPosition?.y})
{paddedPreview && (
Padded dimensions: {paddedPreview.width} × {paddedPreview.height} (original: {imageWidth} × {imageHeight})
Kernel center position in padded image: ({centerPx}, {centerPy})
{Array.from({ length: paddedPreview.height }, (_, py) => Array.from({ length: paddedPreview.width }, (_, px) => { // Show only first 3 channels (RGB) as grayscale const idx = (py * paddedPreview.width + px) * 4 const r = paddedPreview.data[idx] ?? 0 const g = paddedPreview.data[idx + 1] ?? 0 const b = paddedPreview.data[idx + 2] ?? 0 const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b) const isCenter = px === centerPx && py === centerPy const isKernelArea = px >= centerPx - halfKernel && px < centerPx + halfKernel + 1 && py >= centerPy - halfKernel && py < centerPy + halfKernel + 1 return (
= paddedPreview.width - halfKernel || py < halfKernel || py >= paddedPreview.height - halfKernel ? 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 border-gray-200' : 'bg-white text-gray-800 dark:bg-gray-900 dark:text-gray-200 border-gray-200' )} title={`Padded[${px},${py}] = ${gray}`} > {gray}
) }) )}
Kernel Center Kernel Area Padding Area Original Image
)}
)} {!inspectedPosition && (

Click on the image to inspect a convolution step

)}
) }