mihirmithani commited on
Commit ·
f3aa2b6
1
Parent(s): 25e7739
Phase 3
Browse files- src/hooks/useConvolution.ts +27 -9
- src/lib/convolution/engine.ts +51 -0
- src/store/index.ts +16 -6
- src/types/index.ts +3 -0
src/hooks/useConvolution.ts
CHANGED
|
@@ -26,11 +26,21 @@ export function useConvolution() {
|
|
| 26 |
const generatorRef = useRef<Generator<ConvolutionStep> | null>(null)
|
| 27 |
const isPausedRef = useRef(false)
|
| 28 |
const stepModeRef = useRef(false)
|
|
|
|
| 29 |
|
| 30 |
const kernel = selectedKernel?.matrix ?? customKernelMatrix
|
| 31 |
const normalize = selectedKernel?.normalize ?? customKernelNormalize
|
| 32 |
const padding = settings.padding
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
const runConvolution = useCallback(async () => {
|
| 35 |
if (!originalImageData || !kernel) return
|
| 36 |
|
|
@@ -54,26 +64,27 @@ export function useConvolution() {
|
|
| 54 |
generatorRef.current = gen
|
| 55 |
|
| 56 |
const processBatch = () => {
|
| 57 |
-
if (isPausedRef.current
|
| 58 |
animationRef.current = requestAnimationFrame(processBatch)
|
| 59 |
return
|
| 60 |
}
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
let lastStep: ConvolutionStep | null = null
|
|
|
|
| 64 |
|
| 65 |
for (let i = 0; i < batchSize; i++) {
|
| 66 |
const result = gen.next()
|
| 67 |
if (result.done) {
|
| 68 |
if (lastStep && lastStep.x === -1) {
|
| 69 |
// Final step with output image
|
| 70 |
-
const outputImageData = new ImageData(
|
| 71 |
-
new Uint8ClampedArray(originalImageData.data.length),
|
| 72 |
-
originalImageData.width,
|
| 73 |
-
originalImageData.height
|
| 74 |
-
)
|
| 75 |
-
// We need to reconstruct from the generator's final output
|
| 76 |
-
// For now, use the direct convolution
|
| 77 |
const output = convolveImage(originalImageData, kernel, padding, normalize)
|
| 78 |
completeConvolution(output)
|
| 79 |
}
|
|
@@ -81,6 +92,12 @@ export function useConvolution() {
|
|
| 81 |
}
|
| 82 |
lastStep = result.value
|
| 83 |
updateConvolutionStep(result.value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
animationRef.current = requestAnimationFrame(processBatch)
|
|
@@ -102,6 +119,7 @@ export function useConvolution() {
|
|
| 102 |
updateConvolutionProgress,
|
| 103 |
completeConvolution,
|
| 104 |
setConvolutionError,
|
|
|
|
| 105 |
])
|
| 106 |
|
| 107 |
const pause = useCallback(() => {
|
|
|
|
| 26 |
const generatorRef = useRef<Generator<ConvolutionStep> | null>(null)
|
| 27 |
const isPausedRef = useRef(false)
|
| 28 |
const stepModeRef = useRef(false)
|
| 29 |
+
const frameStartRef = useRef<number>(0)
|
| 30 |
|
| 31 |
const kernel = selectedKernel?.matrix ?? customKernelMatrix
|
| 32 |
const normalize = selectedKernel?.normalize ?? customKernelNormalize
|
| 33 |
const padding = settings.padding
|
| 34 |
|
| 35 |
+
// Calculate optimal batch size for ~60fps (16.67ms per frame)
|
| 36 |
+
// Aim to spend ~8ms on computation, leaving ~8ms for rendering
|
| 37 |
+
const getBatchSize = useCallback(() => {
|
| 38 |
+
const baseStepsPerMs = 500 // approximate steps per ms on modern hardware
|
| 39 |
+
const targetComputeTime = 8 // ms
|
| 40 |
+
const speedMultiplier = settings.animationSpeed
|
| 41 |
+
return Math.max(1, Math.floor(baseStepsPerMs * targetComputeTime * speedMultiplier))
|
| 42 |
+
}, [settings.animationSpeed])
|
| 43 |
+
|
| 44 |
const runConvolution = useCallback(async () => {
|
| 45 |
if (!originalImageData || !kernel) return
|
| 46 |
|
|
|
|
| 64 |
generatorRef.current = gen
|
| 65 |
|
| 66 |
const processBatch = () => {
|
| 67 |
+
if (isPausedRef.current) {
|
| 68 |
animationRef.current = requestAnimationFrame(processBatch)
|
| 69 |
return
|
| 70 |
}
|
| 71 |
|
| 72 |
+
if (stepModeRef.current) {
|
| 73 |
+
// In step mode, don't auto-advance
|
| 74 |
+
animationRef.current = requestAnimationFrame(processBatch)
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
frameStartRef.current = performance.now()
|
| 79 |
+
const batchSize = getBatchSize()
|
| 80 |
let lastStep: ConvolutionStep | null = null
|
| 81 |
+
let stepsProcessed = 0
|
| 82 |
|
| 83 |
for (let i = 0; i < batchSize; i++) {
|
| 84 |
const result = gen.next()
|
| 85 |
if (result.done) {
|
| 86 |
if (lastStep && lastStep.x === -1) {
|
| 87 |
// Final step with output image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
const output = convolveImage(originalImageData, kernel, padding, normalize)
|
| 89 |
completeConvolution(output)
|
| 90 |
}
|
|
|
|
| 92 |
}
|
| 93 |
lastStep = result.value
|
| 94 |
updateConvolutionStep(result.value)
|
| 95 |
+
stepsProcessed++
|
| 96 |
+
|
| 97 |
+
// Check if we've spent too much time in this frame (yield to main thread)
|
| 98 |
+
if (performance.now() - frameStartRef.current > 8) {
|
| 99 |
+
break
|
| 100 |
+
}
|
| 101 |
}
|
| 102 |
|
| 103 |
animationRef.current = requestAnimationFrame(processBatch)
|
|
|
|
| 119 |
updateConvolutionProgress,
|
| 120 |
completeConvolution,
|
| 121 |
setConvolutionError,
|
| 122 |
+
getBatchSize,
|
| 123 |
])
|
| 124 |
|
| 125 |
const pause = useCallback(() => {
|
src/lib/convolution/engine.ts
CHANGED
|
@@ -133,6 +133,10 @@ export function* convolutionSteps(
|
|
| 133 |
|
| 134 |
const outputData = new Float32Array(width * height * channels)
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
for (let y = 0; y < height; y++) {
|
| 137 |
for (let x = 0; x < width; x++) {
|
| 138 |
for (let c = 0; c < channels; c++) {
|
|
@@ -166,6 +170,49 @@ export function* convolutionSteps(
|
|
| 166 |
processedPixels++
|
| 167 |
const progress = processedPixels / totalPixels
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
yield {
|
| 170 |
x,
|
| 171 |
y,
|
|
@@ -177,6 +224,8 @@ export function* convolutionSteps(
|
|
| 177 |
outputValue: normalizedValue,
|
| 178 |
normalizedValue,
|
| 179 |
progress,
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
|
| 182 |
if (onProgress) onProgress(progress)
|
|
@@ -202,6 +251,8 @@ export function* convolutionSteps(
|
|
| 202 |
outputValue: 0,
|
| 203 |
normalizedValue: 0,
|
| 204 |
progress: 1,
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
}
|
| 207 |
|
|
|
|
| 133 |
|
| 134 |
const outputData = new Float32Array(width * height * channels)
|
| 135 |
|
| 136 |
+
// For progressive rendering: yield partial output every N rows
|
| 137 |
+
const rowsPerYield = Math.max(1, Math.floor(height / 20)) // ~20 yields per image
|
| 138 |
+
let rowsSinceYield = 0
|
| 139 |
+
|
| 140 |
for (let y = 0; y < height; y++) {
|
| 141 |
for (let x = 0; x < width; x++) {
|
| 142 |
for (let c = 0; c < channels; c++) {
|
|
|
|
| 170 |
processedPixels++
|
| 171 |
const progress = processedPixels / totalPixels
|
| 172 |
|
| 173 |
+
// Create partial output image for progressive rendering
|
| 174 |
+
let partialOutputImageData: ImageData | undefined
|
| 175 |
+
let isPartialOutput = false
|
| 176 |
+
|
| 177 |
+
if (c === channels - 1 && x === width - 1) {
|
| 178 |
+
// End of row - check if we should yield partial output
|
| 179 |
+
rowsSinceYield++
|
| 180 |
+
if (rowsSinceYield >= rowsPerYield || y === height - 1) {
|
| 181 |
+
// Create partial output image
|
| 182 |
+
const partialImageData = new ImageData(width, height)
|
| 183 |
+
for (let i = 0; i < outputData.length; i++) {
|
| 184 |
+
// Only fill up to current progress
|
| 185 |
+
const pixelIdx = Math.floor(i / channels)
|
| 186 |
+
const currentPixelIdx = y * width + x
|
| 187 |
+
if (pixelIdx <= currentPixelIdx) {
|
| 188 |
+
partialImageData.data[i] = Math.max(0, Math.min(255, Math.round(outputData[i] * 255)))
|
| 189 |
+
} else {
|
| 190 |
+
// Unprocessed pixels - show as transparent or original
|
| 191 |
+
partialImageData.data[i] = 0
|
| 192 |
+
partialImageData.data[i + 3] = 0 // transparent alpha for unprocessed
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
// Fill alpha for processed pixels
|
| 196 |
+
for (let py = 0; py <= y; py++) {
|
| 197 |
+
for (let px = 0; px < width; px++) {
|
| 198 |
+
for (let pc = 0; pc < channels; pc++) {
|
| 199 |
+
const idx = (py * width + px) * channels + pc
|
| 200 |
+
const imgIdx = (py * width + px) * 4 + pc
|
| 201 |
+
if (pc < 3) {
|
| 202 |
+
partialImageData.data[imgIdx] = Math.max(0, Math.min(255, Math.round(outputData[idx] * 255)))
|
| 203 |
+
} else {
|
| 204 |
+
partialImageData.data[imgIdx] = 255
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
partialOutputImageData = partialImageData
|
| 211 |
+
isPartialOutput = true
|
| 212 |
+
rowsSinceYield = 0
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
yield {
|
| 217 |
x,
|
| 218 |
y,
|
|
|
|
| 224 |
outputValue: normalizedValue,
|
| 225 |
normalizedValue,
|
| 226 |
progress,
|
| 227 |
+
partialOutputImageData,
|
| 228 |
+
isPartialOutput,
|
| 229 |
}
|
| 230 |
|
| 231 |
if (onProgress) onProgress(progress)
|
|
|
|
| 251 |
outputValue: 0,
|
| 252 |
normalizedValue: 0,
|
| 253 |
progress: 1,
|
| 254 |
+
partialOutputImageData: outputImageData,
|
| 255 |
+
isPartialOutput: true,
|
| 256 |
}
|
| 257 |
}
|
| 258 |
|
src/store/index.ts
CHANGED
|
@@ -265,12 +265,22 @@ export const useAppStore = create<AppStore>()(
|
|
| 265 |
})),
|
| 266 |
|
| 267 |
updateConvolutionStep: (step) =>
|
| 268 |
-
set((state) =>
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
updateConvolutionProgress: (progress, processedPixels, elapsedTime) =>
|
| 276 |
set((state) => ({
|
|
|
|
| 265 |
})),
|
| 266 |
|
| 267 |
updateConvolutionStep: (step) =>
|
| 268 |
+
set((state) => {
|
| 269 |
+
const newState = {
|
| 270 |
+
convolution: {
|
| 271 |
+
...state.convolution,
|
| 272 |
+
currentStep: step,
|
| 273 |
+
},
|
| 274 |
+
}
|
| 275 |
+
// If this step has a partial output image, update the processed image data
|
| 276 |
+
if (step.isPartialOutput && step.partialOutputImageData) {
|
| 277 |
+
return {
|
| 278 |
+
...newState,
|
| 279 |
+
processedImageData: step.partialOutputImageData,
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
return newState
|
| 283 |
+
}),
|
| 284 |
|
| 285 |
updateConvolutionProgress: (progress, processedPixels, elapsedTime) =>
|
| 286 |
set((state) => ({
|
src/types/index.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface ConvolutionStep {
|
|
| 32 |
outputValue: number
|
| 33 |
normalizedValue: number
|
| 34 |
progress: number
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
export interface ConvolutionProgress {
|
|
|
|
| 32 |
outputValue: number
|
| 33 |
normalizedValue: number
|
| 34 |
progress: number
|
| 35 |
+
// Progressive output (optional, set periodically during animation)
|
| 36 |
+
partialOutputImageData?: ImageData
|
| 37 |
+
isPartialOutput?: boolean
|
| 38 |
}
|
| 39 |
|
| 40 |
export interface ConvolutionProgress {
|