fix(masks): show tinted mask edit overlay

This commit is contained in:
syntaxbullet
2026-07-03 18:12:30 +02:00
parent 15f3a934e0
commit daaa2a6667
4 changed files with 130 additions and 3 deletions

View File

@@ -1,10 +1,11 @@
import type { Asset } from "@core/asset"; import type { Asset } from "@core/asset";
import type { ScreenRect, WebGlRendererContext } from "./types"; import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
export type ImageTextureRenderer = { export type ImageTextureRenderer = {
syncAssets(assets: readonly Asset[]): void; syncAssets(assets: readonly Asset[]): void;
render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect): boolean; render(asset: Asset, rect: ScreenRect, clipRect?: ScreenRect): boolean;
renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect): boolean; renderMasked(asset: Asset, rect: ScreenRect, maskAsset: Asset, maskRect: ScreenRect, clipRect?: ScreenRect): boolean;
renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect): boolean;
dispose(): void; dispose(): void;
}; };
@@ -31,13 +32,18 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
const maskedMaskTexCoordLocation = gl.getAttribLocation(maskedProgram, "a_maskTexCoord"); const maskedMaskTexCoordLocation = gl.getAttribLocation(maskedProgram, "a_maskTexCoord");
const maskedSamplerLocation = gl.getUniformLocation(maskedProgram, "u_image"); const maskedSamplerLocation = gl.getUniformLocation(maskedProgram, "u_image");
const maskedMaskSamplerLocation = gl.getUniformLocation(maskedProgram, "u_mask"); const maskedMaskSamplerLocation = gl.getUniformLocation(maskedProgram, "u_mask");
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 positionBuffer = gl.createBuffer();
const texCoordBuffer = gl.createBuffer(); const texCoordBuffer = gl.createBuffer();
const maskTexCoordBuffer = gl.createBuffer(); const maskTexCoordBuffer = gl.createBuffer();
const textures = new Map<string, TextureEntry>(); const textures = new Map<string, TextureEntry>();
let disposed = false; let disposed = false;
if (!positionBuffer || !texCoordBuffer || !maskTexCoordBuffer || !samplerLocation || !maskedSamplerLocation || !maskedMaskSamplerLocation) throw new Error("Failed to create image texture renderer"); if (!positionBuffer || !texCoordBuffer || !maskTexCoordBuffer || !samplerLocation || !maskedSamplerLocation || !maskedMaskSamplerLocation || !tintedSamplerLocation || !tintedColorLocation) throw new Error("Failed to create image texture renderer");
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); 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); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
@@ -127,6 +133,39 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
gl.disable(gl.BLEND); gl.disable(gl.BLEND);
return true; 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);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
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() { dispose() {
disposed = true; disposed = true;
for (const entry of textures.values()) disposeEntry(gl, entry); for (const entry of textures.values()) disposeEntry(gl, entry);
@@ -135,6 +174,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
gl.deleteBuffer(maskTexCoordBuffer); gl.deleteBuffer(maskTexCoordBuffer);
gl.deleteProgram(program); gl.deleteProgram(program);
gl.deleteProgram(maskedProgram); gl.deleteProgram(maskedProgram);
gl.deleteProgram(tintedProgram);
}, },
}; };
} }
@@ -304,6 +344,51 @@ function createProgram(gl: WebGL2RenderingContext) {
return program; return program;
} }
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 maskAlpha = texture(u_image, v_texCoord).a;
outColor = vec4(u_color.rgb, u_color.a * maskAlpha);
}`,
);
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 createMaskedProgram(gl: WebGL2RenderingContext) { function createMaskedProgram(gl: WebGL2RenderingContext) {
const vertexShader = compileShader( const vertexShader = compileShader(
gl, gl,

View File

@@ -29,7 +29,7 @@ function renderLayer(
maskLayerIds: ReadonlySet<string>, maskLayerIds: ReadonlySet<string>,
) { ) {
const editingMaskLayer = editor.maskEditLayerId === layer.id; const editingMaskLayer = editor.maskEditLayerId === layer.id;
if (!layer.visible || (maskLayerIds.has(layer.id) && !editingMaskLayer)) return; if (!layer.visible || maskLayerIds.has(layer.id)) return;
const effectiveClipRect = resolveLayerClipRect(context, document, editor.viewport, layer, clipRect); const effectiveClipRect = resolveLayerClipRect(context, document, editor.viewport, layer, clipRect);
if (!effectiveClipRect) return; if (!effectiveClipRect) return;

View File

@@ -0,0 +1,40 @@
import type { ImageDocument } from "@core/document";
import type { Layer } from "@core/layer";
import type { EditorState } from "@editor/state";
import { resolveTransformTargetBounds } from "@editor/transform-targets";
import type { ImageTextureRenderer } from "./image-textures";
import { documentRectToScreenRect } from "./screen-rect";
import type { WebGlRendererContext } from "./types";
const maskOverlayColor = [0.25, 0.65, 1, 0.35] as const;
export function renderMaskEditOverlay(
context: WebGlRendererContext,
document: ImageDocument,
editor: EditorState,
imageTextureRenderer: ImageTextureRenderer,
) {
const layerId = editor.maskEditLayerId;
if (!layerId) return;
const layer = findLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
if (!layer || layer.type === "group") return;
const bounds = resolveTransformTargetBounds(document, { type: "layer", id: layer.id });
const asset = document.assets.find((candidate) => candidate.id === layer.assetId);
if (!bounds || !asset) return;
const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport);
imageTextureRenderer.renderTinted(asset, rect, maskOverlayColor);
}
function findLayer(layers: readonly Layer[], layerId: string): Layer | undefined {
for (const layer of layers) {
if (layer.id === layerId) return layer;
if (layer.type === "group") {
const child = findLayer(layer.children, layerId);
if (child) return child;
}
}
return undefined;
}

View File

@@ -3,6 +3,7 @@ import type { EditorState } from "@editor/state";
import { renderArtboard } from "./artboard"; import { renderArtboard } from "./artboard";
import { createImageTextureRenderer } from "./image-textures"; import { createImageTextureRenderer } from "./image-textures";
import { renderLayers } from "./layers"; import { renderLayers } from "./layers";
import { renderMaskEditOverlay } from "./mask-edit-overlay";
import { renderSelectionOverlay } from "./selection"; import { renderSelectionOverlay } from "./selection";
import { renderTransformControls } from "./transform-controls"; import { renderTransformControls } from "./transform-controls";
import type { WebGlRendererContext } from "./types"; import type { WebGlRendererContext } from "./types";
@@ -60,6 +61,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
} }
imageTextureRenderer.syncAssets(frame.document.assets); imageTextureRenderer.syncAssets(frame.document.assets);
renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer); renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer);
renderMaskEditOverlay(rendererContext, frame.document, frame.editor, imageTextureRenderer);
renderSelectionOverlay(rendererContext, frame.document, frame.editor); renderSelectionOverlay(rendererContext, frame.document, frame.editor);
renderTransformControls(rendererContext, frame.document, frame.editor); renderTransformControls(rendererContext, frame.document, frame.editor);