kernel-convolution-visualizer / src /hooks /useConvolution.ts
mihirmithani
Phase 3
94f3ed9
Raw History Blame
6.51 kB
import { useCallback, useRef, useEffect } from 'react'
import { useAppStore } from '@/store'
import { convolutionSteps, convolveImage } from '@/lib/convolution/engine'
import type { PaddingMode, ConvolutionStep } from '@/types'
export function useConvolution() {
const {
originalImageData,
selectedKernel,
customKernelMatrix,
customKernelNormalize,
settings,
convolution,
startConvolution,
pauseConvolution,
resumeConvolution,
stopConvolution,
updateConvolutionStep,
updateConvolutionProgress,
completeConvolution,
setConvolutionError,
resetConvolution,
} = useAppStore()
const animationRef = useRef<number | null>(null)
const generatorRef = useRef<Generator<ConvolutionStep> | null>(null)
const isPausedRef = useRef(false)
const stepModeRef = useRef(false)
const frameStartRef = useRef<number>(0)
const kernel = selectedKernel?.matrix ?? customKernelMatrix
const normalize = selectedKernel?.normalize ?? customKernelNormalize
const padding = settings.padding
// Calculate optimal batch size for ~60fps (16.67ms per frame)
// Aim to spend ~8ms on computation, leaving ~8ms for rendering
const getBatchSize = useCallback(() => {
const baseStepsPerMs = 50 // reduced from 500 for better responsiveness
const targetComputeTime = 8 // ms
const speedMultiplier = settings.animationSpeed
return Math.max(1, Math.floor(baseStepsPerMs * targetComputeTime * speedMultiplier))
}, [settings.animationSpeed])
const runConvolution = useCallback(async () => {
if (!originalImageData || !kernel) return
try {
startConvolution()
const extendedImage = {
data: originalImageData.data,
width: originalImageData.width,
height: originalImageData.height,
channels: 4,
}
const gen = convolutionSteps(extendedImage, kernel, padding, normalize, (progress) => {
const totalPixels = originalImageData.width * originalImageData.height * 4
const processedPixels = Math.round(progress * totalPixels)
const elapsedTime = Date.now() - convolution.startTime
updateConvolutionProgress(progress, processedPixels, elapsedTime)
})
generatorRef.current = gen
let lastFrameTime = 0
const processBatch = () => {
if (isPausedRef.current) {
animationRef.current = requestAnimationFrame(processBatch)
return
}
if (stepModeRef.current) {
// In step mode, don't auto-advance
animationRef.current = requestAnimationFrame(processBatch)
return
}
const now = performance.now()
const frameTime = now - lastFrameTime
const minFrameTime = 1000 / 60 // ~16.67ms for 60fps
// If we're running too fast, schedule next frame appropriately
if (frameTime < minFrameTime) {
setTimeout(() => {
animationRef.current = requestAnimationFrame(processBatch)
}, minFrameTime - frameTime)
return
}
lastFrameTime = now
frameStartRef.current = now
const batchSize = getBatchSize()
let lastStep: ConvolutionStep | null = null
let stepsProcessed = 0
for (let i = 0; i < batchSize; i++) {
const result = gen.next()
if (result.done) {
if (lastStep && lastStep.x === -1) {
// Final step with output image
const output = convolveImage(originalImageData, kernel, padding, normalize)
completeConvolution(output)
}
return
}
lastStep = result.value
updateConvolutionStep(result.value)
stepsProcessed++
// Check if we've spent too much time in this frame (yield to main thread)
// Reduced from 8ms to 4ms for better responsiveness
if (performance.now() - frameStartRef.current > 4) {
break
}
}
animationRef.current = requestAnimationFrame(processBatch)
}
animationRef.current = requestAnimationFrame(processBatch)
} catch (error) {
setConvolutionError(error instanceof Error ? error.message : 'Convolution failed')
}
}, [
originalImageData,
kernel,
normalize,
padding,
settings.animationSpeed,
convolution.startTime,
startConvolution,
updateConvolutionStep,
updateConvolutionProgress,
completeConvolution,
setConvolutionError,
getBatchSize,
])
const pause = useCallback(() => {
isPausedRef.current = true
pauseConvolution()
}, [pauseConvolution])
const resume = useCallback(() => {
isPausedRef.current = false
resumeConvolution()
}, [resumeConvolution])
const stop = useCallback(() => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
animationRef.current = null
}
isPausedRef.current = false
stepModeRef.current = false
generatorRef.current = null
stopConvolution()
}, [stopConvolution])
const step = useCallback(() => {
if (!generatorRef.current) return
// Temporarily enable step mode for this single step
const wasPaused = isPausedRef.current
isPausedRef.current = true
stepModeRef.current = true
const result = generatorRef.current.next()
if (!result.done) {
updateConvolutionStep(result.value)
} else if (result.value && result.value.x === -1) {
const output = convolveImage(originalImageData!, kernel, padding, normalize)
completeConvolution(output)
}
// Restore paused state, but keep step mode enabled if it was explicitly toggled
isPausedRef.current = wasPaused
}, [originalImageData, kernel, padding, normalize, updateConvolutionStep, completeConvolution])
const reset = useCallback(() => {
stop()
resetConvolution()
}, [stop, resetConvolution])
const toggleStepMode = useCallback((enabled: boolean) => {
stepModeRef.current = enabled
}, [])
useEffect(() => {
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current)
}
}
}, [])
return {
convolution,
runConvolution,
pause,
resume,
stop,
step,
reset,
toggleStepMode,
isRunning: convolution.status === 'running',
isPaused: convolution.status === 'paused',
isCompleted: convolution.status === 'completed',
canRun: !!originalImageData && !!kernel && convolution.status !== 'running',
}
}