- Refactor LayersSheet component to support mask editing state and improve layer visibility handling. - Introduce functions to collect mask layer IDs and count display layers excluding masks. - Update BrushControls to include mask view mode options and a Done button for exiting mask editing. - Modify brush session handling to support brush previews when editing masks. - Implement a new BrushPreviewRenderer for rendering brush strokes with visual feedback. - Add document geometry utilities for transforming points and resolving layer bounds. - Ensure proper cleanup of brush preview on pointer leave and other interactions.
733 lines
27 KiB
TypeScript
733 lines
27 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): boolean;
|
|
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect): boolean;
|
|
renderMaskRevealPreview(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, opacity: number, clipRect?: ScreenRect): boolean;
|
|
renderMaskVisualization(maskAsset: Asset, maskRect: ScreenRect, mode: MaskVisualizationMode, color?: RgbaColor, clipRect?: ScreenRect): boolean;
|
|
renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect): 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 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 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 ||
|
|
!maskedSamplerLocation ||
|
|
!maskedMaskSamplerLocation ||
|
|
!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) {
|
|
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.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) {
|
|
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.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) {
|
|
const clampedOpacity = Math.max(0, Math.min(1, 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(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) {
|
|
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);
|
|
|
|
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 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;
|
|
in vec2 v_texCoord;
|
|
out vec4 outColor;
|
|
void main() {
|
|
outColor = texture(u_image, v_texCoord);
|
|
}`,
|
|
);
|
|
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() {
|
|
float maskAlpha = texture(u_mask, v_texCoord).a;
|
|
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);
|
|
float hiddenMaskAlpha = 1.0 - texture(u_mask, v_maskTexCoord).a;
|
|
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;
|
|
in vec2 v_texCoord;
|
|
in vec2 v_maskTexCoord;
|
|
out vec4 outColor;
|
|
void main() {
|
|
vec4 color = texture(u_image, v_texCoord);
|
|
float maskAlpha = texture(u_mask, v_maskTexCoord).a;
|
|
float alpha = color.a * maskAlpha;
|
|
outColor = vec4(color.rgb * maskAlpha, alpha);
|
|
}`,
|
|
);
|
|
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;
|
|
}
|