Files
image-studio/renderer/adjustment-pass.ts
2026-07-11 12:31:06 +02:00

112 lines
4.9 KiB
TypeScript

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;
}