mihirmithani
Phase 3
94f3ed9
Raw History Blame
9.05 kB
'use client'
import { useRef, useEffect } from 'react'
import { useAppStore } from '@/store'
import { PADDING_MODES } from '@/lib/kernels'
import { Select, Card, CardContent } from '@/components/ui'
import { cn } from '@/utils/helpers'
import { Settings, Layers, Grid, ZoomIn, ZoomOut } from 'lucide-react'
export function OverlayControls() {
const { settings, updateSettings, toggleOverlay, setPadding } = useAppStore()
const { showOverlay, padding } = settings
return (
<Card className="w-full">
<CardContent className="p-4 space-y-4">
<h3 className="font-medium flex items-center gap-2">
<Settings className="h-4 w-4" />
Visualization Overlays
</h3>
<div className="grid grid-cols-2 gap-2">
{Object.entries(showOverlay).map(([key, value]) => (
<label key={key} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={value}
onChange={() => toggleOverlay(key as keyof typeof showOverlay)}
className="rounded border-gray-300"
/>
<span className="text-sm capitalize">{key.replace(/([A-Z])/g, ' $1')}</span>
</label>
))}
</div>
</CardContent>
</Card>
)
}
export function PaddingControls() {
const { settings, setPadding, originalImageData, selectedKernel } = useAppStore()
const { padding } = settings
return (
<Card className="w-full">
<CardContent className="p-4 space-y-4">
<h3 className="font-medium flex items-center gap-2">
<Grid className="h-4 w-4" />
Padding Strategy
</h3>
<Select
value={padding}
onChange={(e) => setPadding(e.target.value as typeof padding)}
options={PADDING_MODES.map((p) => ({ value: p.value, label: p.label }))}
placeholder="Select padding mode"
/>
<div className="text-sm text-gray-500 dark:text-gray-400">
{PADDING_MODES.find((p) => p.value === padding)?.description}
</div>
<PaddingVisualization
imageData={originalImageData}
padding={padding}
kernelSize={selectedKernel?.size ?? 3}
/>
</CardContent>
</Card>
)
}
function PaddingVisualization({
imageData,
padding,
kernelSize
}: {
imageData: ImageData | null
padding: 'zero' | 'reflect' | 'replicate' | 'wrap'
kernelSize: number
}) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const halfKernel = Math.floor(kernelSize / 2)
const previewSize = 120
useEffect(() => {
if (!imageData || !canvasRef.current) return
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
if (!ctx) return
// Small preview canvas
canvas.width = previewSize
canvas.height = previewSize
ctx.clearRect(0, 0, previewSize, previewSize)
const scale = Math.min(previewSize / (imageData.width + 2 * halfKernel), previewSize / (imageData.height + 2 * halfKernel))
const paddedWidth = (imageData.width + 2 * halfKernel) * scale
const paddedHeight = (imageData.height + 2 * halfKernel) * scale
const offsetX = (previewSize - paddedWidth) / 2
const offsetY = (previewSize - paddedHeight) / 2
// Draw padding visualization
const getPixel = (x: number, y: number, channel: number): number => {
let px = x
let py = y
const { width, height, data } = imageData
switch (padding) {
case 'zero':
if (px < 0 || px >= width || py < 0 || py >= height) return 0
break
case 'replicate':
px = Math.max(0, Math.min(px, width - 1))
py = Math.max(0, Math.min(py, height - 1))
break
case 'reflect':
if (px < 0) px = -px
if (px >= width) px = 2 * width - px - 2
if (py < 0) py = -py
if (py >= height) py = 2 * height - py - 2
px = Math.max(0, Math.min(px, width - 1))
py = Math.max(0, Math.min(py, height - 1))
break
case 'wrap':
px = ((px % width) + width) % width
py = ((py % height) + height) % height
break
}
const index = (py * width + px) * 4 + channel
return data[index]
}
// Draw padded image
for (let py = -halfKernel; py < imageData.height + halfKernel; py++) {
for (let px = -halfKernel; px < imageData.width + halfKernel; px++) {
const r = getPixel(px, py, 0)
const g = getPixel(px, py, 1)
const b = getPixel(px, py, 2)
const x = offsetX + (px + halfKernel) * scale
const y = offsetY + (py + halfKernel) * scale
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
ctx.fillRect(x, y, Math.ceil(scale), Math.ceil(scale))
}
}
// Draw original image border
ctx.strokeStyle = 'rgba(59, 130, 246, 0.8)'
ctx.lineWidth = 2
ctx.setLineDash([4, 2])
ctx.strokeRect(
offsetX + halfKernel * scale,
offsetY + halfKernel * scale,
imageData.width * scale,
imageData.height * scale
)
ctx.setLineDash([])
// Draw kernel center indicator
ctx.fillStyle = 'rgba(239, 68, 68, 0.8)'
ctx.beginPath()
ctx.arc(
offsetX + halfKernel * scale,
offsetY + halfKernel * scale,
4,
0,
Math.PI * 2
)
ctx.fill()
// Draw kernel area outline
ctx.strokeStyle = 'rgba(239, 68, 68, 0.5)'
ctx.lineWidth = 1
ctx.strokeRect(
offsetX,
offsetY,
paddedWidth,
paddedHeight
)
}, [imageData, padding, kernelSize])
return (
<div className="space-y-2">
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400">Padding Preview</h4>
<div className="relative">
<canvas
ref={canvasRef}
width={previewSize}
height={previewSize}
className="bg-gray-100 dark:bg-gray-800 rounded border dark:border-gray-700"
/>
<div className="absolute bottom-1 left-1 right-1 flex justify-between text-[10px] text-gray-400 px-2">
<span>Original Image</span>
<span className="text-red-400">Kernel Center</span>
<span>Padding Area</span>
</div>
</div>
</div>
)
}
export function KernelInfoPanel() {
const { selectedKernel } = useAppStore()
if (!selectedKernel) {
return (
<Card className="w-full">
<CardContent className="p-4 text-center text-gray-500">
No kernel selected
</CardContent>
</Card>
)
}
const { name, category, matrix, size, normalize, description, formula, reference } = selectedKernel
return (
<Card className="w-full">
<CardContent className="p-4 space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="font-medium">{name}</h3>
<div className="flex items-center gap-2 mt-1">
<span className="px-2 py-0.5 text-xs bg-blue-100 text-blue-800 rounded dark:bg-blue-900/30 dark:text-blue-300">
{category}
</span>
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-800 rounded dark:bg-gray-800 dark:text-gray-300">
{size}×{size}
</span>
{normalize && (
<span className="px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded dark:bg-green-900/30 dark:text-green-300">
Normalized
</span>
)}
</div>
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">{description}</p>
{formula && (
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded font-mono text-sm">
<span className="text-gray-500">Formula: </span>
<span>{formula}</span>
</div>
)}
<div className="space-y-2">
<h4 className="text-sm font-medium">Kernel Matrix</h4>
<div className="grid gap-1" style={{ gridTemplateColumns: `repeat(${size}, 1fr)` }}>
{matrix.map((row, i) =>
row.map((val, j) => (
<div
key={`${i}-${j}`}
className={cn(
'p-2 text-center text-sm font-mono rounded',
val > 0 ? 'bg-green-50 text-green-800 dark:bg-green-900/30 dark:text-green-300' : 'bg-red-50 text-red-800 dark:bg-red-900/30 dark:text-red-300'
)}
>
{val.toFixed(val === Math.round(val) ? 0 : 2)}
</div>
))
)}
</div>
</div>
{reference && (
<a
href={reference}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline flex items-center gap-1"
>
<Layers className="h-3 w-3" />
Reference
</a>
)}
</CardContent>
</Card>
)
}