import type { ColorAdjustment } from "@core/adjustment-layer"; import type { ScreenRect, WebGlRendererContext } from "./types"; export type AdjustmentPass = { apply(adjustment: ColorAdjustment, opacity: number, clip: ScreenRect): void; dispose(): void; }; const vertexSource = `#version 300 es in vec2 a_position; void main(){ gl_Position=vec4(a_position,0.,1.); }`; const fragmentSource = `#version 300 es precision highp float; uniform sampler2D u_pixels; uniform vec2 u_size; uniform float u_brightness,u_contrast,u_saturation,u_opacity; uniform vec3 u_balance; out vec4 outColor; void main(){ vec2 uv=gl_FragCoord.xy/u_size; vec4 original=texture(u_pixels,uv); vec3 c=original.rgb+vec3(u_brightness)+u_balance; c=(c-0.5)*(1.0+u_contrast)+0.5; float l=dot(c,vec3(.2126,.7152,.0722)); c=clamp(vec3(l)+(c-vec3(l))*(1.0+u_saturation),0.0,1.0); outColor=vec4(mix(original.rgb,c,u_opacity),original.a); }`; /** Renderer-owned framebuffer adjustment resources, reused across layers/frames. */ export function createAdjustmentPass(context: WebGlRendererContext): AdjustmentPass { const { gl, canvas } = context; const program = createProgram(gl, vertexSource, fragmentSource); const texture = gl.createTexture(); const buffer = gl.createBuffer(); if (!program || !texture || !buffer) throw new Error("Failed to create adjustment pass"); const position = gl.getAttribLocation(program, "a_position"); const size = requiredUniform(gl, program, "u_size"); const brightness = requiredUniform(gl, program, "u_brightness"); const contrast = requiredUniform(gl, program, "u_contrast"); const saturation = requiredUniform(gl, program, "u_saturation"); const opacityLocation = requiredUniform(gl, program, "u_opacity"); const balance = requiredUniform(gl, program, "u_balance"); const pixels = requiredUniform(gl, program, "u_pixels"); let allocatedWidth = 0; let allocatedHeight = 0; let disposed = false; gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); return { apply(adjustment, opacity, clip) { if (disposed || canvas.width < 1 || canvas.height < 1) return; gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); if (allocatedWidth !== canvas.width || allocatedHeight !== canvas.height) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, canvas.width, canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); allocatedWidth = canvas.width; allocatedHeight = canvas.height; } gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, canvas.width, canvas.height); gl.useProgram(program); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.enableVertexAttribArray(position); gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0); gl.uniform1i(pixels, 0); gl.uniform2f(size, canvas.width, canvas.height); gl.uniform1f(brightness, adjustment.brightness); gl.uniform1f(contrast, adjustment.contrast); gl.uniform1f(saturation, adjustment.saturation); gl.uniform1f(opacityLocation, Math.max(0, Math.min(1, opacity))); gl.uniform3f(balance, adjustment.colorBalance.red, adjustment.colorBalance.green, adjustment.colorBalance.blue); gl.scissor(clip.x, canvas.height - clip.y - clip.h, clip.w, clip.h); gl.disable(gl.BLEND); gl.drawArrays(gl.TRIANGLES, 0, 6); gl.enable(gl.BLEND); }, dispose() { if (disposed) return; disposed = true; gl.deleteTexture(texture); gl.deleteBuffer(buffer); gl.deleteProgram(program); }, }; } function requiredUniform(gl: WebGL2RenderingContext, program: WebGLProgram, name: string) { const location = gl.getUniformLocation(program, name); if (!location) throw new Error(`Missing adjustment uniform: ${name}`); return location; } function createProgram(gl: WebGL2RenderingContext, vertex: string, fragment: string) { const vertexShader = compile(gl, gl.VERTEX_SHADER, vertex); const fragmentShader = compile(gl, gl.FRAGMENT_SHADER, fragment); if (!vertexShader || !fragmentShader) return undefined; const program = gl.createProgram(); if (!program) return undefined; gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.deleteShader(vertexShader); gl.deleteShader(fragmentShader); if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program; gl.deleteProgram(program); return undefined; } function compile(gl: WebGL2RenderingContext, type: number, source: string) { const shader = gl.createShader(type); if (!shader) return undefined; gl.shaderSource(shader, source); gl.compileShader(shader); if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader; gl.deleteShader(shader); return undefined; }