feat(masks): add mask editing mode
This commit is contained in:
@@ -4,6 +4,7 @@ import type { ScreenRect, WebGlRendererContext } from "./types";
|
||||
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;
|
||||
dispose(): void;
|
||||
};
|
||||
|
||||
@@ -24,12 +25,19 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
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 positionBuffer = gl.createBuffer();
|
||||
const texCoordBuffer = gl.createBuffer();
|
||||
const maskTexCoordBuffer = gl.createBuffer();
|
||||
const textures = new Map<string, TextureEntry>();
|
||||
let disposed = false;
|
||||
|
||||
if (!positionBuffer || !texCoordBuffer || !samplerLocation) throw new Error("Failed to create image texture renderer");
|
||||
if (!positionBuffer || !texCoordBuffer || !maskTexCoordBuffer || !samplerLocation || !maskedSamplerLocation || !maskedMaskSamplerLocation) 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);
|
||||
@@ -68,6 +76,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
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);
|
||||
|
||||
@@ -75,12 +84,57 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
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);
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
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;
|
||||
},
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -185,6 +239,19 @@ function createTexture(gl: WebGL2RenderingContext, image: HTMLImageElement) {
|
||||
return texture;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -237,6 +304,56 @@ function createProgram(gl: WebGL2RenderingContext) {
|
||||
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;
|
||||
outColor = vec4(color.rgb, color.a * maskAlpha);
|
||||
}`,
|
||||
);
|
||||
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");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Layer } from "@core/layer";
|
||||
import type { ViewportState } from "@editor/state";
|
||||
import type { EditorState, ViewportState } from "@editor/state";
|
||||
import { resolveTransformTargetBounds } from "@editor/transform-targets";
|
||||
import { clearScreenRect } from "./clear-rect";
|
||||
import type { ImageTextureRenderer } from "./image-textures";
|
||||
@@ -10,39 +10,45 @@ import type { RgbaColor, ScreenRect, WebGlRendererContext } from "./types";
|
||||
const imageLayerColor: RgbaColor = [0.38, 0.42, 0.5, 1];
|
||||
const imageLayerInsetColor: RgbaColor = [0.48, 0.54, 0.64, 1];
|
||||
|
||||
export function renderLayers(context: WebGlRendererContext, document: ImageDocument, viewport: ViewportState, imageTextureRenderer: ImageTextureRenderer) {
|
||||
export function renderLayers(context: WebGlRendererContext, document: ImageDocument, editor: EditorState, imageTextureRenderer: ImageTextureRenderer) {
|
||||
for (const artboard of document.artboards) {
|
||||
if (!artboard.visible) continue;
|
||||
const clipRect = documentRectToScreenRect(context.canvas, artboard.bounds, viewport);
|
||||
const clipRect = documentRectToScreenRect(context.canvas, artboard.bounds, editor.viewport);
|
||||
const maskLayerIds = collectMaskLayerIds(artboard.layers);
|
||||
for (const layer of artboard.layers) renderLayer(context, document, viewport, layer, imageTextureRenderer, clipRect, maskLayerIds);
|
||||
for (const layer of artboard.layers) renderLayer(context, document, editor, layer, imageTextureRenderer, clipRect, maskLayerIds);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLayer(
|
||||
context: WebGlRendererContext,
|
||||
document: ImageDocument,
|
||||
viewport: ViewportState,
|
||||
editor: EditorState,
|
||||
layer: Layer,
|
||||
imageTextureRenderer: ImageTextureRenderer,
|
||||
clipRect: ScreenRect,
|
||||
maskLayerIds: ReadonlySet<string>,
|
||||
) {
|
||||
if (!layer.visible || maskLayerIds.has(layer.id)) return;
|
||||
const editingMaskLayer = editor.maskEditLayerId === layer.id;
|
||||
if (!layer.visible || (maskLayerIds.has(layer.id) && !editingMaskLayer)) return;
|
||||
|
||||
const effectiveClipRect = resolveLayerClipRect(context, document, viewport, layer, clipRect);
|
||||
const effectiveClipRect = resolveLayerClipRect(context, document, editor.viewport, layer, clipRect);
|
||||
if (!effectiveClipRect) return;
|
||||
|
||||
if (layer.type === "group") {
|
||||
for (const child of layer.children) renderLayer(context, document, viewport, child, imageTextureRenderer, effectiveClipRect, maskLayerIds);
|
||||
for (const child of layer.children) renderLayer(context, document, editor, child, imageTextureRenderer, effectiveClipRect, maskLayerIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = resolveTransformTargetBounds(document, { type: "layer", id: layer.id });
|
||||
if (!bounds) return;
|
||||
|
||||
const rect = documentRectToScreenRect(context.canvas, bounds, viewport);
|
||||
const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport);
|
||||
const asset = document.assets.find((candidate) => candidate.id === layer.assetId);
|
||||
const maskLayer = !editingMaskLayer && layer.clippingMask ? findLayer(document, layer.clippingMask.maskLayerId) : undefined;
|
||||
const maskAsset = maskLayer && maskLayer.type !== "group" ? document.assets.find((candidate) => candidate.id === maskLayer.assetId) : undefined;
|
||||
const maskBounds = maskLayer ? resolveTransformTargetBounds(document, { type: "layer", id: maskLayer.id }) : undefined;
|
||||
const maskRect = maskBounds ? documentRectToScreenRect(context.canvas, maskBounds, editor.viewport) : undefined;
|
||||
if (asset && maskAsset && maskRect && imageTextureRenderer.renderMasked(asset, rect, maskAsset, maskRect, effectiveClipRect)) return;
|
||||
if (asset && imageTextureRenderer.render(asset, rect, effectiveClipRect)) return;
|
||||
|
||||
const fallbackRect = intersectScreenRects(rect, effectiveClipRect);
|
||||
@@ -67,6 +73,25 @@ function resolveLayerClipRect(
|
||||
return intersectScreenRects(clipRect, documentRectToScreenRect(context.canvas, maskBounds, viewport));
|
||||
}
|
||||
|
||||
function findLayer(document: ImageDocument, layerId: string): Layer | undefined {
|
||||
for (const artboard of document.artboards) {
|
||||
const layer = findLayerInTree(artboard.layers, layerId);
|
||||
if (layer) return layer;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findLayerInTree(layers: readonly Layer[], layerId: string): Layer | undefined {
|
||||
for (const layer of layers) {
|
||||
if (layer.id === layerId) return layer;
|
||||
if (layer.type === "group") {
|
||||
const child = findLayerInTree(layer.children, layerId);
|
||||
if (child) return child;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectMaskLayerIds(layers: readonly Layer[], ids = new Set<string>()) {
|
||||
for (const layer of layers) {
|
||||
if (layer.clippingMask) ids.add(layer.clippingMask.maskLayerId);
|
||||
|
||||
@@ -59,7 +59,7 @@ export function createRenderer(canvas: HTMLCanvasElement, backend: RendererBacke
|
||||
if (artboard.visible) renderArtboard(rendererContext, artboard, frame.editor.viewport);
|
||||
}
|
||||
imageTextureRenderer.syncAssets(frame.document.assets);
|
||||
renderLayers(rendererContext, frame.document, frame.editor.viewport, imageTextureRenderer);
|
||||
renderLayers(rendererContext, frame.document, frame.editor, imageTextureRenderer);
|
||||
|
||||
renderSelectionOverlay(rendererContext, frame.document, frame.editor);
|
||||
renderTransformControls(rendererContext, frame.document, frame.editor);
|
||||
|
||||
Reference in New Issue
Block a user