// GPU-accelerated augmentation using WebGL // Applies brightness, contrast, and noise operations on the GPU // Check if WebGL is available let webglAvailable: boolean | null = null let glContext: WebGLRenderingContext | null = null let glCanvas: HTMLCanvasElement | null = null export function isWebGLAvailable(): boolean { if (webglAvailable !== null) return webglAvailable try { const canvas = document.createElement('canvas') const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') webglAvailable = gl !== null return webglAvailable } catch { webglAvailable = false return false } } // Shader source code const vertexShaderSource = ` attribute vec2 a_position; attribute vec2 a_texCoord; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord; } ` const fragmentShaderSource = ` precision mediump float; uniform sampler2D u_image; uniform float u_brightness; uniform float u_contrast; uniform float u_noiseAmount; uniform float u_seed; varying vec2 v_texCoord; // Simple pseudo-random function float random(vec2 co) { return fract(sin(dot(co.xy + u_seed, vec2(12.9898, 78.233))) * 43758.5453); } void main() { vec4 color = texture2D(u_image, v_texCoord); // Apply brightness (additive) color.rgb += u_brightness; // Apply contrast (multiplicative from midpoint) color.rgb = (color.rgb - 0.5) * u_contrast + 0.5; // Apply noise if (u_noiseAmount > 0.0) { float noise = (random(v_texCoord) - 0.5) * u_noiseAmount; color.rgb += noise; } // Clamp values color.rgb = clamp(color.rgb, 0.0, 1.0); gl_FragColor = color; } ` // Compile shader function compileShader(gl: WebGLRenderingContext, source: string, type: number): WebGLShader | null { const shader = gl.createShader(type) if (!shader) return null gl.shaderSource(shader, source) gl.compileShader(shader) if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(shader)) gl.deleteShader(shader) return null } return shader } // Create shader program function createProgram(gl: WebGLRenderingContext): WebGLProgram | null { const vertexShader = compileShader(gl, vertexShaderSource, gl.VERTEX_SHADER) const fragmentShader = compileShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER) if (!vertexShader || !fragmentShader) return null const program = gl.createProgram() if (!program) return null gl.attachShader(program, vertexShader) gl.attachShader(program, fragmentShader) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error('Program link error:', gl.getProgramInfoLog(program)) gl.deleteProgram(program) return null } return program } // Initialize WebGL context and program let program: WebGLProgram | null = null let positionBuffer: WebGLBuffer | null = null let texCoordBuffer: WebGLBuffer | null = null function initWebGL(width: number, height: number): boolean { if (glCanvas && glContext && program) { // Resize if needed if (glCanvas.width !== width || glCanvas.height !== height) { glCanvas.width = width glCanvas.height = height glContext.viewport(0, 0, width, height) } return true } try { glCanvas = document.createElement('canvas') glCanvas.width = width glCanvas.height = height glContext = glCanvas.getContext('webgl', { preserveDrawingBuffer: true }) as WebGLRenderingContext if (!glContext) { console.warn('WebGL not available') return false } program = createProgram(glContext) if (!program) { console.warn('Failed to create WebGL program') return false } // Create buffers const gl = glContext // Position buffer (full screen quad) positionBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1 ]), gl.STATIC_DRAW) // Texture coordinate buffer texCoordBuffer = gl.createBuffer() gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0 ]), gl.STATIC_DRAW) return true } catch (err) { console.warn('WebGL initialization failed:', err) return false } } export interface GPUAugmentOptions { brightness?: number // -1 to 1 (0 = no change) contrast?: number // 0 to 2 (1 = no change) noiseAmount?: number // 0 to 1 seed?: number } /** * Apply GPU-accelerated augmentation to an image * Returns the augmented canvas, or null if WebGL is not available */ export function applyGPUAugmentation( sourceCanvas: HTMLCanvasElement, options: GPUAugmentOptions ): HTMLCanvasElement | null { const { brightness = 0, contrast = 1, noiseAmount = 0, seed = Math.random() } = options // Skip if no augmentation needed if (brightness === 0 && contrast === 1 && noiseAmount === 0) { return sourceCanvas } if (!initWebGL(sourceCanvas.width, sourceCanvas.height)) { return null // Fallback to CPU } const gl = glContext! try { gl.useProgram(program!) // Create texture from source canvas const texture = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, texture) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, sourceCanvas) // Set up attributes const positionLocation = gl.getAttribLocation(program!, 'a_position') const texCoordLocation = gl.getAttribLocation(program!, 'a_texCoord') gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer!) gl.enableVertexAttribArray(positionLocation) gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0) gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer!) gl.enableVertexAttribArray(texCoordLocation) gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0) // Set uniforms gl.uniform1f(gl.getUniformLocation(program!, 'u_brightness'), brightness) gl.uniform1f(gl.getUniformLocation(program!, 'u_contrast'), contrast) gl.uniform1f(gl.getUniformLocation(program!, 'u_noiseAmount'), noiseAmount) gl.uniform1f(gl.getUniformLocation(program!, 'u_seed'), seed) // Draw gl.drawArrays(gl.TRIANGLES, 0, 6) // Clean up texture gl.deleteTexture(texture) return glCanvas! } catch (err) { console.warn('GPU augmentation failed:', err) return null } } /** * Apply GPU augmentation and return the result as ImageData */ export function applyGPUAugmentationToImageData( imageData: ImageData, options: GPUAugmentOptions ): ImageData | null { // Create temporary canvas with the image data const tempCanvas = document.createElement('canvas') tempCanvas.width = imageData.width tempCanvas.height = imageData.height const ctx = tempCanvas.getContext('2d') if (!ctx) return null ctx.putImageData(imageData, 0, 0) const resultCanvas = applyGPUAugmentation(tempCanvas, options) if (!resultCanvas) return null const resultCtx = resultCanvas.getContext('2d') || resultCanvas.getContext('webgl') // For WebGL canvas, we need to read pixels differently if (glContext) { const pixels = new Uint8Array(imageData.width * imageData.height * 4) glContext.readPixels(0, 0, imageData.width, imageData.height, glContext.RGBA, glContext.UNSIGNED_BYTE, pixels) // Flip Y axis (WebGL has origin at bottom-left) const flipped = new Uint8ClampedArray(pixels.length) const rowSize = imageData.width * 4 for (let y = 0; y < imageData.height; y++) { const srcRow = (imageData.height - 1 - y) * rowSize const dstRow = y * rowSize flipped.set(pixels.subarray(srcRow, srcRow + rowSize), dstRow) } return new ImageData(flipped, imageData.width, imageData.height) } return null } /** * Clean up WebGL resources */ export function cleanupGPU() { if (glContext && program) { glContext.deleteProgram(program) if (positionBuffer) glContext.deleteBuffer(positionBuffer) if (texCoordBuffer) glContext.deleteBuffer(texCoordBuffer) } program = null positionBuffer = null texCoordBuffer = null glContext = null glCanvas = null }