Files
image-studio/renderer/image-textures.ts
syntaxbullet 5eaf37ba28 feat: enhance layer mask handling and opacity management
- Introduced `getLayerMask` utility to streamline layer mask retrieval.
- Updated layer rendering logic to incorporate layer masks and opacity adjustments.
- Added functionality to prevent dropping a group into its descendants.
- Enhanced image texture rendering to support opacity parameters across various rendering functions.
- Implemented group layer bounds application for better scaling and positioning.
- Added tests to ensure correct behavior when handling layer masks and group layers.
- Created new types for asset generation provenance to track generated assets more effectively.
2026-07-05 16:06:23 +02:00

755 lines
28 KiB
TypeScript

import type { Asset } from "@core/asset";
import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
export type MaskVisualizationMode = "blackWhite" | "alpha" | "hiddenOverlay";
export type ImageTextureRenderer = {
syncAssets(assets: readonly Asset[]): void;
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect, opacity?: number): boolean;
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect, opacity?: number): boolean;
renderMaskRevealPreview(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, opacity: number, clipRect?: ScreenRect, layerOpacity?: number): boolean;
renderMaskVisualization(maskAsset: Asset, maskRect: ScreenRect, mode: MaskVisualizationMode, color?: RgbaColor, clipRect?: ScreenRect): boolean;
renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect, opacity?: number): boolean;
dispose(): void;
};
type TextureState =
| { status: "loading"; image: HTMLImageElement }
| { status: "ready"; texture: WebGLTexture }
| { status: "error" };
type TextureEntry = {
source: string;
current: TextureState;
previousTexture?: WebGLTexture;
};
type MaskVisualizationResources = {
program: WebGLProgram;
positionLocation: number;
texCoordLocation: number;
samplerLocation: WebGLUniformLocation;
modeLocation: WebGLUniformLocation;
colorLocation: WebGLUniformLocation;
};
export function createImageTextureRenderer(context: WebGlRendererContext, invalidate: () => void): ImageTextureRenderer {
const { gl } = context;
const program = createProgram(gl);
const positionLocation = gl.getAttribLocation(program, "a_position");
const texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
const samplerLocation = gl.getUniformLocation(program, "u_image");
const opacityLocation = gl.getUniformLocation(program, "u_opacity");
const maskedProgram = createMaskedProgram(gl);
const maskedPositionLocation = gl.getAttribLocation(maskedProgram, "a_position");
const maskedTexCoordLocation = gl.getAttribLocation(maskedProgram, "a_texCoord");
const maskedMaskTexCoordLocation = gl.getAttribLocation(maskedProgram, "a_maskTexCoord");
const maskedSamplerLocation = gl.getUniformLocation(maskedProgram, "u_image");
const maskedMaskSamplerLocation = gl.getUniformLocation(maskedProgram, "u_mask");
const maskedOpacityLocation = gl.getUniformLocation(maskedProgram, "u_opacity");
const maskRevealPreviewProgram = createMaskRevealPreviewProgram(gl);
const maskRevealPreviewPositionLocation = gl.getAttribLocation(maskRevealPreviewProgram, "a_position");
const maskRevealPreviewTexCoordLocation = gl.getAttribLocation(maskRevealPreviewProgram, "a_texCoord");
const maskRevealPreviewMaskTexCoordLocation = gl.getAttribLocation(maskRevealPreviewProgram, "a_maskTexCoord");
const maskRevealPreviewSamplerLocation = gl.getUniformLocation(maskRevealPreviewProgram, "u_image");
const maskRevealPreviewMaskSamplerLocation = gl.getUniformLocation(maskRevealPreviewProgram, "u_mask");
const maskRevealPreviewOpacityLocation = gl.getUniformLocation(maskRevealPreviewProgram, "u_opacity");
const tintedProgram = createTintedProgram(gl);
const tintedPositionLocation = gl.getAttribLocation(tintedProgram, "a_position");
const tintedTexCoordLocation = gl.getAttribLocation(tintedProgram, "a_texCoord");
const tintedSamplerLocation = gl.getUniformLocation(tintedProgram, "u_image");
const tintedColorLocation = gl.getUniformLocation(tintedProgram, "u_color");
const positionBuffer = gl.createBuffer();
const texCoordBuffer = gl.createBuffer();
const maskTexCoordBuffer = gl.createBuffer();
const textures = new Map<string, TextureEntry>();
let maskVisualizationResources: MaskVisualizationResources | "failed" | undefined;
let disposed = false;
if (
!positionBuffer ||
!texCoordBuffer ||
!maskTexCoordBuffer ||
!samplerLocation ||
!opacityLocation ||
!maskedSamplerLocation ||
!maskedMaskSamplerLocation ||
!maskedOpacityLocation ||
!maskRevealPreviewSamplerLocation ||
!maskRevealPreviewMaskSamplerLocation ||
!maskRevealPreviewOpacityLocation ||
!tintedSamplerLocation ||
!tintedColorLocation
) throw new Error("Failed to create image texture renderer");
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
return {
syncAssets(assets) {
const activeIds = new Set(assets.map((asset) => asset.id));
for (const [assetId, entry] of textures) {
if (!activeIds.has(assetId)) {
disposeEntry(gl, entry);
textures.delete(assetId);
}
}
},
render(asset, rect, clipRect, opacity = 1) {
const clampedOpacity = clampOpacity(opacity);
if (clampedOpacity <= 0) return true;
const drawRect = clipRect ? intersectScreenRects(rect, clipRect) : rect;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
const texture = renderableTexture(entry);
if (!texture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
enablePremultipliedAlphaBlending(gl);
gl.useProgram(program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(samplerLocation, 0);
gl.uniform1f(opacityLocation, clampedOpacity);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, rect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
return true;
},
renderMasked(asset, rect, maskAsset, maskRect, clipRect, opacity = 1) {
const clampedOpacity = clampOpacity(opacity);
if (clampedOpacity <= 0) return true;
const clippedRect = clipRect ? intersectScreenRects(rect, clipRect) : rect;
const drawRect = clippedRect ? intersectScreenRects(clippedRect, maskRect) : undefined;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
const maskEntry = getTextureEntry(context, textures, maskAsset, invalidate, () => disposed);
const texture = renderableTexture(entry);
const maskTexture = renderableTexture(maskEntry);
if (!texture || !maskTexture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
enablePremultipliedAlphaBlending(gl);
gl.useProgram(maskedProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(maskedSamplerLocation, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, maskTexture);
gl.uniform1i(maskedMaskSamplerLocation, 1);
gl.uniform1f(maskedOpacityLocation, clampedOpacity);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, drawRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskedPositionLocation);
gl.vertexAttribPointer(maskedPositionLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoordsForRect(drawRect, rect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskedTexCoordLocation);
gl.vertexAttribPointer(maskedTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, maskTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoordsForRect(drawRect, maskRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskedMaskTexCoordLocation);
gl.vertexAttribPointer(maskedMaskTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
return true;
},
renderMaskRevealPreview(asset, rect, maskAsset, maskRect, opacity, clipRect, layerOpacity = 1) {
const clampedOpacity = clampOpacity(opacity) * clampOpacity(layerOpacity);
if (clampedOpacity <= 0) return true;
const clippedRect = clipRect ? intersectScreenRects(rect, clipRect) : rect;
const drawRect = clippedRect ? intersectScreenRects(clippedRect, maskRect) : undefined;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
const maskEntry = getTextureEntry(context, textures, maskAsset, invalidate, () => disposed);
const texture = renderableTexture(entry);
const maskTexture = renderableTexture(maskEntry);
if (!texture || !maskTexture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
enablePremultipliedAlphaBlending(gl);
gl.useProgram(maskRevealPreviewProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(maskRevealPreviewSamplerLocation, 0);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, maskTexture);
gl.uniform1i(maskRevealPreviewMaskSamplerLocation, 1);
gl.uniform1f(maskRevealPreviewOpacityLocation, clampedOpacity);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, drawRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskRevealPreviewPositionLocation);
gl.vertexAttribPointer(maskRevealPreviewPositionLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoordsForRect(drawRect, rect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskRevealPreviewTexCoordLocation);
gl.vertexAttribPointer(maskRevealPreviewTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, maskTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoordsForRect(drawRect, maskRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(maskRevealPreviewMaskTexCoordLocation);
gl.vertexAttribPointer(maskRevealPreviewMaskTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
return true;
},
renderMaskVisualization(maskAsset, maskRect, mode, color = [1, 1, 1, 1], clipRect) {
const drawRect = clipRect ? intersectScreenRects(maskRect, clipRect) : maskRect;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const resources = getMaskVisualizationResources(gl, () => maskVisualizationResources, (nextResources) => {
maskVisualizationResources = nextResources;
});
if (!resources) return false;
const maskEntry = getTextureEntry(context, textures, maskAsset, invalidate, () => disposed);
const maskTexture = renderableTexture(maskEntry);
if (!maskTexture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
enablePremultipliedAlphaBlending(gl);
gl.useProgram(resources.program);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, maskTexture);
gl.uniform1i(resources.samplerLocation, 0);
gl.uniform1i(resources.modeLocation, maskVisualizationModeValue(mode));
gl.uniform4fv(resources.colorLocation, color);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, drawRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(resources.positionLocation);
gl.vertexAttribPointer(resources.positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, texCoordsForRect(drawRect, maskRect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(resources.texCoordLocation);
gl.vertexAttribPointer(resources.texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
return true;
},
renderTinted(asset, rect, color, clipRect, opacity = 1) {
const clampedOpacity = clampOpacity(opacity);
if (clampedOpacity <= 0) return true;
const drawRect = clipRect ? intersectScreenRects(rect, clipRect) : rect;
if (!drawRect || drawRect.w <= 0 || drawRect.h <= 0) return true;
const entry = getTextureEntry(context, textures, asset, invalidate, () => disposed);
const texture = renderableTexture(entry);
if (!texture) return false;
gl.enable(gl.SCISSOR_TEST);
gl.scissor(drawRect.x, context.canvas.height - drawRect.y - drawRect.h, drawRect.w, drawRect.h);
enablePremultipliedAlphaBlending(gl);
gl.useProgram(tintedProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(tintedSamplerLocation, 0);
gl.uniform4fv(tintedColorLocation, [color[0], color[1], color[2], color[3] * clampedOpacity]);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, rectVertices(context.canvas, rect), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(tintedPositionLocation);
gl.vertexAttribPointer(tintedPositionLocation, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, fullTexCoords(), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(tintedTexCoordLocation);
gl.vertexAttribPointer(tintedTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
gl.disable(gl.BLEND);
return true;
},
dispose() {
disposed = true;
for (const entry of textures.values()) disposeEntry(gl, entry);
gl.deleteBuffer(positionBuffer);
gl.deleteBuffer(texCoordBuffer);
gl.deleteBuffer(maskTexCoordBuffer);
gl.deleteProgram(program);
gl.deleteProgram(maskedProgram);
gl.deleteProgram(maskRevealPreviewProgram);
if (maskVisualizationResources && maskVisualizationResources !== "failed") gl.deleteProgram(maskVisualizationResources.program);
gl.deleteProgram(tintedProgram);
},
};
}
function getTextureEntry(
context: WebGlRendererContext,
textures: Map<string, TextureEntry>,
asset: Asset,
invalidate: () => void,
isDisposed: () => boolean,
): TextureEntry {
const cached = textures.get(asset.id);
if (cached && cached.source === asset.source) return cached;
if (cached) {
if (cached.current.status === "ready") {
if (cached.previousTexture) context.gl.deleteTexture(cached.previousTexture);
cached.previousTexture = cached.current.texture;
} else {
disposeState(context.gl, cached.current);
}
cached.source = asset.source;
cached.current = loadTextureState(context, asset, cached, invalidate, isDisposed);
return cached;
}
const entry: TextureEntry = { source: asset.source, current: { status: "error" } };
entry.current = loadTextureState(context, asset, entry, invalidate, isDisposed);
textures.set(asset.id, entry);
return entry;
}
function loadTextureState(
context: WebGlRendererContext,
asset: Asset,
entry: TextureEntry,
invalidate: () => void,
isDisposed: () => boolean,
): TextureState {
const image = new Image();
const source = asset.source;
image.onload = () => {
if (isDisposed() || entry.source !== source) return;
const texture = createTexture(context.gl, image);
if (entry.previousTexture) context.gl.deleteTexture(entry.previousTexture);
entry.previousTexture = undefined;
entry.current = { status: "ready", texture };
invalidate();
};
image.onerror = () => {
if (isDisposed() || entry.source !== source) return;
entry.current = { status: "error" };
invalidate();
};
image.src = source;
return { status: "loading", image };
}
function renderableTexture(entry: TextureEntry): WebGLTexture | undefined {
if (entry.current.status === "ready") return entry.current.texture;
return entry.previousTexture;
}
function disposeEntry(gl: WebGL2RenderingContext, entry: TextureEntry) {
disposeState(gl, entry.current);
if (entry.previousTexture) gl.deleteTexture(entry.previousTexture);
}
function disposeState(gl: WebGL2RenderingContext, state: TextureState) {
if (state.status === "ready") {
gl.deleteTexture(state.texture);
return;
}
if (state.status === "loading") {
state.image.onload = null;
state.image.onerror = null;
}
}
function intersectScreenRects(a: ScreenRect, b: ScreenRect): ScreenRect | undefined {
const x1 = Math.max(a.x, b.x);
const y1 = Math.max(a.y, b.y);
const x2 = Math.min(a.x + a.w, b.x + b.w);
const y2 = Math.min(a.y + a.h, b.y + b.h);
if (x2 <= x1 || y2 <= y1) return undefined;
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}
function createTexture(gl: WebGL2RenderingContext, image: HTMLImageElement) {
const texture = gl.createTexture();
if (!texture) throw new Error("Failed to create image texture");
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.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
return texture;
}
function enablePremultipliedAlphaBlending(gl: WebGL2RenderingContext) {
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
function clampOpacity(opacity: number) {
return Math.max(0, Math.min(1, opacity));
}
function fullTexCoords() {
return new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]);
}
function texCoordsForRect(drawRect: ScreenRect, sourceRect: ScreenRect) {
const x1 = (drawRect.x - sourceRect.x) / sourceRect.w;
const x2 = (drawRect.x + drawRect.w - sourceRect.x) / sourceRect.w;
const y1 = (drawRect.y - sourceRect.y) / sourceRect.h;
const y2 = (drawRect.y + drawRect.h - sourceRect.y) / sourceRect.h;
return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]);
}
function rectVertices(canvas: HTMLCanvasElement, rect: ScreenRect) {
const x1 = (rect.x / canvas.width) * 2 - 1;
const x2 = ((rect.x + rect.w) / canvas.width) * 2 - 1;
const y1 = 1 - (rect.y / canvas.height) * 2;
const y2 = 1 - ((rect.y + rect.h) / canvas.height) * 2;
return new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]);
}
function createProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float;
uniform sampler2D u_image;
uniform float u_opacity;
in vec2 v_texCoord;
out vec4 outColor;
void main() {
outColor = texture(u_image, v_texCoord) * u_opacity;
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create image shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown program link error";
gl.deleteProgram(program);
throw new Error(message);
}
return program;
}
function getMaskVisualizationResources(
gl: WebGL2RenderingContext,
getResources: () => MaskVisualizationResources | "failed" | undefined,
setResources: (resources: MaskVisualizationResources | "failed") => void,
): MaskVisualizationResources | undefined {
const currentResources = getResources();
if (currentResources === "failed") return undefined;
if (currentResources) return currentResources;
try {
const resources = createMaskVisualizationProgram(gl);
setResources(resources);
return resources;
} catch {
setResources("failed");
return undefined;
}
}
function maskVisualizationModeValue(mode: MaskVisualizationMode) {
switch (mode) {
case "blackWhite":
return 0;
case "alpha":
return 1;
case "hiddenOverlay":
return 2;
}
}
function createMaskVisualizationProgram(gl: WebGL2RenderingContext): MaskVisualizationResources {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float;
uniform sampler2D u_mask;
uniform int u_mode;
uniform vec4 u_color;
in vec2 v_texCoord;
out vec4 outColor;
void main() {
vec4 maskColor = texture(u_mask, v_texCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if (u_mode == 0) {
float value = step(0.5, maskAlpha);
outColor = vec4(value, value, value, 1.0);
return;
}
if (u_mode == 1) {
outColor = vec4(maskAlpha, maskAlpha, maskAlpha, 1.0);
return;
}
float alpha = (1.0 - maskAlpha) * u_color.a;
outColor = vec4(u_color.rgb * alpha, alpha);
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create mask visualization shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown mask visualization program link error";
gl.deleteProgram(program);
throw new Error(message);
}
const samplerLocation = gl.getUniformLocation(program, "u_mask");
const modeLocation = gl.getUniformLocation(program, "u_mode");
const colorLocation = gl.getUniformLocation(program, "u_color");
if (!samplerLocation || !modeLocation || !colorLocation) {
gl.deleteProgram(program);
throw new Error("Failed to resolve mask visualization shader uniforms");
}
return {
program,
positionLocation: gl.getAttribLocation(program, "a_position"),
texCoordLocation: gl.getAttribLocation(program, "a_texCoord"),
samplerLocation,
modeLocation,
colorLocation,
};
}
function createTintedProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float;
uniform sampler2D u_image;
uniform vec4 u_color;
in vec2 v_texCoord;
out vec4 outColor;
void main() {
float alpha = u_color.a * texture(u_image, v_texCoord).a;
outColor = vec4(u_color.rgb * alpha, alpha);
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create tinted image shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown tinted program link error";
gl.deleteProgram(program);
throw new Error(message);
}
return program;
}
function createMaskRevealPreviewProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
in vec2 a_maskTexCoord;
out vec2 v_texCoord;
out vec2 v_maskTexCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
v_maskTexCoord = a_maskTexCoord;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float;
uniform sampler2D u_image;
uniform sampler2D u_mask;
uniform float u_opacity;
in vec2 v_texCoord;
in vec2 v_maskTexCoord;
out vec4 outColor;
void main() {
vec4 color = texture(u_image, v_texCoord);
vec4 maskColor = texture(u_mask, v_maskTexCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
float hiddenMaskAlpha = 1.0 - maskAlpha;
float previewAlpha = clamp(hiddenMaskAlpha * u_opacity, 0.0, 1.0);
outColor = vec4(color.rgb * previewAlpha, color.a * previewAlpha);
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create mask reveal preview shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown mask reveal preview program link error";
gl.deleteProgram(program);
throw new Error(message);
}
return program;
}
function createMaskedProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader(
gl,
gl.VERTEX_SHADER,
`#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
in vec2 a_maskTexCoord;
out vec2 v_texCoord;
out vec2 v_maskTexCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
v_maskTexCoord = a_maskTexCoord;
}`,
);
const fragmentShader = compileShader(
gl,
gl.FRAGMENT_SHADER,
`#version 300 es
precision mediump float;
uniform sampler2D u_image;
uniform sampler2D u_mask;
uniform float u_opacity;
in vec2 v_texCoord;
in vec2 v_maskTexCoord;
out vec4 outColor;
void main() {
vec4 color = texture(u_image, v_texCoord);
vec4 maskColor = texture(u_mask, v_maskTexCoord);
float maskAlpha = maskColor.a * dot(maskColor.rgb, vec3(0.2126, 0.7152, 0.0722));
float alpha = color.a * maskAlpha;
outColor = vec4(color.rgb * maskAlpha, alpha) * u_opacity;
}`,
);
const program = gl.createProgram();
if (!program) throw new Error("Failed to create masked image shader program");
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const message = gl.getProgramInfoLog(program) ?? "Unknown masked program link error";
gl.deleteProgram(program);
throw new Error(message);
}
return program;
}
function compileShader(gl: WebGL2RenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (!shader) throw new Error("Failed to create shader");
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const message = gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
gl.deleteShader(shader);
throw new Error(message);
}
return shader;
}