112 lines
4.9 KiB
TypeScript
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;
|
|
}
|