"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { COLORS, VizFrame } from "./common"; import { useReducedMotion } from "@/lib/hooks/useReducedMotion"; type KernelKey = "edge" | "sharpen" | "blur" | "identity"; const KERNELS: Record = { edge: [ [-1, -1, -1], [-1, 8, -1], [-1, -1, -1], ], sharpen: [ [0, -1, 0], [-1, 5, -1], [0, -1, 0], ], blur: [ [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], [1 / 9, 1 / 9, 1 / 9], ], identity: [ [0, 0, 0], [0, 1, 0], [0, 0, 0], ], }; function genImage(N: number) { const img: number[][] = []; for (let i = 0; i < N; i++) { const row: number[] = []; for (let j = 0; j < N; j++) { const cx = N / 2; const cy = N / 2; const r = Math.sqrt((i - cy) ** 2 + (j - cx) ** 2); const v = Math.max(0, 1 - r / (N * 0.45)) * 0.7 + Math.sin(j * 0.4) * 0.1 + (i % 2 === 0 ? 0.05 : 0); row.push(Math.max(0, Math.min(1, v))); } img.push(row); } return img; } function convolve(img: number[][], k: number[][]) { const N = img.length; const out: number[][] = Array.from({ length: N }, () => Array(N).fill(0)); for (let i = 1; i < N - 1; i++) { for (let j = 1; j < N - 1; j++) { let acc = 0; for (let di = -1; di <= 1; di++) { for (let dj = -1; dj <= 1; dj++) { acc += img[i + di][j + dj] * k[di + 1][dj + 1]; } } out[i][j] = acc; } } return out; } export function ConvKernel({ N = 12, width = 880, height = 460, }: { N?: number; width?: number; height?: number; }) { const inputCanvas = useRef(null); const outputCanvas = useRef(null); const overlayInput = useRef(null); const overlayOutput = useRef(null); const [kernelKey, setKernelKey] = useState("edge"); const [pos, setPos] = useState({ i: 1, j: 1 }); const [running, setRunning] = useState(true); const img = useMemo(() => genImage(N), [N]); const k = KERNELS[kernelKey]; const out = useMemo(() => convolve(img, k), [img, k]); // Draw bases useEffect(() => { const cell = 22; const drawGrid = ( canvas: HTMLCanvasElement, grid: number[][], norm = false, ) => { const ctx = canvas.getContext("2d")!; ctx.clearRect(0, 0, canvas.width, canvas.height); let lo = Infinity; let hi = -Infinity; if (norm) { for (const row of grid) for (const v of row) { if (v < lo) lo = v; if (v > hi) hi = v; } } for (let i = 0; i < N; i++) { for (let j = 0; j < N; j++) { const v = norm ? (grid[i][j] - lo) / (hi - lo + 1e-9) : grid[i][j]; const g = Math.floor(255 - v * 220); ctx.fillStyle = `rgb(${g},${g},${g})`; ctx.fillRect(j * cell, i * cell, cell, cell); ctx.strokeStyle = "rgba(14,14,16,0.06)"; ctx.strokeRect(j * cell, i * cell, cell, cell); } } }; if (inputCanvas.current) drawGrid(inputCanvas.current, img); if (outputCanvas.current) drawGrid(outputCanvas.current, out, true); }, [img, out, N]); const reduce = useReducedMotion(); useEffect(() => { if (!running || reduce) return; const id = setInterval(() => { setPos(({ i, j }) => { let nj = j + 1; let ni = i; if (nj > N - 2) { nj = 1; ni = i + 1; } if (ni > N - 2) ni = 1; return { i: ni, j: nj }; }); }, 220); return () => clearInterval(id); }, [running, N, reduce]); // Draw highlight overlays useEffect(() => { const cell = 22; const drawHi = (canvas: HTMLCanvasElement, x: number, y: number, w: number, h: number) => { const ctx = canvas.getContext("2d")!; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = COLORS.honey; ctx.lineWidth = 2; ctx.fillStyle = "rgba(232,181,60,0.18)"; ctx.fillRect(x, y, w, h); ctx.strokeRect(x, y, w, h); }; if (overlayInput.current) { drawHi(overlayInput.current, (pos.j - 1) * cell, (pos.i - 1) * cell, cell * 3, cell * 3); } if (overlayOutput.current) { drawHi(overlayOutput.current, pos.j * cell, pos.i * cell, cell, cell); } }, [pos, N]); const cellPx = 22; const px = N * cellPx; return (
input
{/* Kernel display */}
kernel
{k.map((row, i) => ( {row.map((v, j) => ( ))} ))}
{Math.round(v * 100) / 100}
output[i,j] = Σ
output
{(Object.keys(KERNELS) as KernelKey[]).map((kk) => ( ))}
); }