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.
This commit is contained in:
@@ -5,11 +5,11 @@ 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;
|
||||
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): boolean;
|
||||
renderTinted(asset: Asset, rect: ScreenRect, color: RgbaColor, clipRect?: ScreenRect, opacity?: number): boolean;
|
||||
dispose(): void;
|
||||
};
|
||||
|
||||
@@ -39,12 +39,14 @@ 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 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");
|
||||
@@ -69,8 +71,10 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
!texCoordBuffer ||
|
||||
!maskTexCoordBuffer ||
|
||||
!samplerLocation ||
|
||||
!opacityLocation ||
|
||||
!maskedSamplerLocation ||
|
||||
!maskedMaskSamplerLocation ||
|
||||
!maskedOpacityLocation ||
|
||||
!maskRevealPreviewSamplerLocation ||
|
||||
!maskRevealPreviewMaskSamplerLocation ||
|
||||
!maskRevealPreviewOpacityLocation ||
|
||||
@@ -91,7 +95,9 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
}
|
||||
}
|
||||
},
|
||||
render(asset, rect, clipRect) {
|
||||
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;
|
||||
|
||||
@@ -107,6 +113,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
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);
|
||||
@@ -122,7 +129,9 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.disable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
renderMasked(asset, rect, maskAsset, maskRect, clipRect) {
|
||||
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;
|
||||
@@ -144,6 +153,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
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);
|
||||
@@ -164,8 +174,8 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.disable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
renderMaskRevealPreview(asset, rect, maskAsset, maskRect, opacity, clipRect) {
|
||||
const clampedOpacity = Math.max(0, Math.min(1, opacity));
|
||||
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;
|
||||
@@ -248,7 +258,9 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.disable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
renderTinted(asset, rect, color, clipRect) {
|
||||
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;
|
||||
|
||||
@@ -264,7 +276,7 @@ export function createImageTextureRenderer(context: WebGlRendererContext, invali
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.uniform1i(tintedSamplerLocation, 0);
|
||||
gl.uniform4fv(tintedColorLocation, color);
|
||||
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);
|
||||
@@ -401,6 +413,10 @@ function enablePremultipliedAlphaBlending(gl: WebGL2RenderingContext) {
|
||||
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]);
|
||||
}
|
||||
@@ -442,10 +458,11 @@ function createProgram(gl: WebGL2RenderingContext) {
|
||||
`#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);
|
||||
outColor = texture(u_image, v_texCoord) * u_opacity;
|
||||
}`,
|
||||
);
|
||||
const program = gl.createProgram();
|
||||
@@ -691,6 +708,7 @@ function createMaskedProgram(gl: WebGL2RenderingContext) {
|
||||
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;
|
||||
@@ -699,7 +717,7 @@ function createMaskedProgram(gl: WebGL2RenderingContext) {
|
||||
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);
|
||||
outColor = vec4(color.rgb * maskAlpha, alpha) * u_opacity;
|
||||
}`,
|
||||
);
|
||||
const program = gl.createProgram();
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Asset } from "@core/asset";
|
||||
import type { ImageDocument } from "@core/document";
|
||||
import type { Rect } from "@core/geometry";
|
||||
import type { Layer } from "@core/layer";
|
||||
import { getLayerMask } from "@core/layer-mask-utils";
|
||||
import { createDocumentReadIndex, forEachLayerBackToFront, resolveIndexedLayerBounds, type DocumentReadIndex } from "@editor/document-indexes";
|
||||
import type { EditorState, GenerationCandidate, MaskViewMode, ViewportState } from "@editor/state";
|
||||
import { clearScreenRect } from "./clear-rect";
|
||||
@@ -12,6 +13,7 @@ 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];
|
||||
const hiddenMaskOverlayColor: RgbaColor = [1, 0.08, 0.08, 0.45];
|
||||
const comparisonDividerColor: RgbaColor = [1, 1, 1, 0.9];
|
||||
const maskRevealPreviewOpacity = 0.28;
|
||||
|
||||
export function renderLayers(context: WebGlRendererContext, document: ImageDocument, editor: EditorState, imageTextureRenderer: ImageTextureRenderer) {
|
||||
@@ -22,7 +24,7 @@ export function renderLayers(context: WebGlRendererContext, document: ImageDocum
|
||||
if (!artboard.visible) continue;
|
||||
const clipRect = documentRectToScreenRect(context.canvas, artboard.bounds, editor.viewport);
|
||||
const maskLayerIds = documentIndex.maskLayerIdsByArtboardId.get(artboard.id) ?? emptyLayerIds;
|
||||
forEachLayerBackToFront(artboard.layers, (layer) => renderLayer(context, documentIndex, editor, layer, imageTextureRenderer, clipRect, maskLayerIds));
|
||||
forEachLayerBackToFront(artboard.layers, (layer) => renderLayer(context, documentIndex, editor, layer, imageTextureRenderer, clipRect, maskLayerIds, 1));
|
||||
if (generationCandidate?.placement.artboardId === artboard.id) renderGenerationCandidatePreview(context, editor, generationCandidate, imageTextureRenderer, clipRect);
|
||||
}
|
||||
}
|
||||
@@ -39,17 +41,20 @@ function renderLayer(
|
||||
imageTextureRenderer: ImageTextureRenderer,
|
||||
clipRect: ScreenRect,
|
||||
maskLayerIds: ReadonlySet<string>,
|
||||
inheritedOpacity: number,
|
||||
) {
|
||||
const editingMaskLayer = editor.maskEdit?.maskLayerId === layer.id;
|
||||
const maskViewMode = editor.maskEdit?.viewMode ?? "composite";
|
||||
const isolatedMaskView = isIsolatedMaskView(maskViewMode);
|
||||
if (!layer.visible || maskLayerIds.has(layer.id)) return;
|
||||
const effectiveOpacity = inheritedOpacity * layer.opacity;
|
||||
if (effectiveOpacity <= 0) return;
|
||||
|
||||
const effectiveClipRect = resolveLayerClipRect(context, documentIndex, editor.viewport, layer, clipRect);
|
||||
if (!effectiveClipRect) return;
|
||||
|
||||
if (layer.type === "group") {
|
||||
forEachLayerBackToFront(layer.children, (child) => renderLayer(context, documentIndex, editor, child, imageTextureRenderer, effectiveClipRect, maskLayerIds));
|
||||
forEachLayerBackToFront(layer.children, (child) => renderLayer(context, documentIndex, editor, child, imageTextureRenderer, effectiveClipRect, maskLayerIds, effectiveOpacity));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,33 +65,34 @@ function renderLayer(
|
||||
|
||||
const rect = documentRectToScreenRect(context.canvas, bounds, editor.viewport);
|
||||
const asset = assetWithBrushStrokePreview(documentIndex.assetById.get(layer.assetId), editor);
|
||||
const maskLayer = !editingMaskLayer && layer.clippingMask ? documentIndex.layerById.get(layer.clippingMask.maskLayerId) : undefined;
|
||||
const layerMask = getLayerMask(layer);
|
||||
const maskLayer = !editingMaskLayer && layerMask?.enabled ? documentIndex.layerById.get(layerMask.maskLayerId) : undefined;
|
||||
const maskAsset = assetWithBrushStrokePreview(maskLayer && maskLayer.type !== "group" ? documentIndex.assetById.get(maskLayer.assetId) : undefined, editor);
|
||||
const maskBounds = maskLayer ? resolveIndexedLayerBounds(documentIndex, maskLayer) : undefined;
|
||||
const maskRect = maskBounds ? documentRectToScreenRect(context.canvas, maskBounds, editor.viewport) : undefined;
|
||||
const activeMaskTarget = Boolean(editor.maskEdit?.targetLayerId === layer.id && editor.maskEdit.maskLayerId === layer.clippingMask?.maskLayerId);
|
||||
const activeMaskTarget = Boolean(editor.maskEdit?.targetLayerId === layer.id && editor.maskEdit.maskLayerId === layerMask?.maskLayerId);
|
||||
const showMaskRevealPreview = editor.tools.activeTool === "brush" && activeMaskTarget && maskViewMode === "composite";
|
||||
|
||||
if (asset && maskAsset && maskRect && activeMaskTarget) {
|
||||
if (maskViewMode === "blackWhite" && imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "blackWhite", undefined, effectiveClipRect)) return;
|
||||
if (maskViewMode === "alpha" && imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "alpha", undefined, effectiveClipRect)) return;
|
||||
if (maskViewMode === "overlay" && imageTextureRenderer.render(asset, rect, effectiveClipRect)) {
|
||||
if (maskViewMode === "overlay" && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity)) {
|
||||
imageTextureRenderer.renderMaskVisualization(maskAsset, maskRect, "hiddenOverlay", hiddenMaskOverlayColor, effectiveClipRect);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (asset && maskAsset && maskRect && imageTextureRenderer.renderMasked(asset, rect, maskAsset, maskRect, effectiveClipRect)) {
|
||||
if (showMaskRevealPreview) imageTextureRenderer.renderMaskRevealPreview(asset, rect, maskAsset, maskRect, maskRevealPreviewOpacity, effectiveClipRect);
|
||||
if (asset && maskAsset && maskRect && imageTextureRenderer.renderMasked(asset, rect, maskAsset, maskRect, effectiveClipRect, effectiveOpacity)) {
|
||||
if (showMaskRevealPreview) imageTextureRenderer.renderMaskRevealPreview(asset, rect, maskAsset, maskRect, maskRevealPreviewOpacity, effectiveClipRect, effectiveOpacity);
|
||||
return;
|
||||
}
|
||||
if (asset && imageTextureRenderer.render(asset, rect, effectiveClipRect)) return;
|
||||
if (asset && imageTextureRenderer.render(asset, rect, effectiveClipRect, effectiveOpacity)) return;
|
||||
|
||||
const fallbackRect = intersectScreenRects(rect, effectiveClipRect);
|
||||
if (!fallbackRect) return;
|
||||
clearScreenRect(context, fallbackRect, imageLayerColor);
|
||||
clearScreenRect(context, fallbackRect, withOpacity(imageLayerColor, effectiveOpacity));
|
||||
const insetRect = intersectScreenRects({ x: rect.x + 4, y: rect.y + 4, w: Math.max(0, rect.w - 8), h: Math.max(0, rect.h - 8) }, effectiveClipRect);
|
||||
if (insetRect) clearScreenRect(context, insetRect, imageLayerInsetColor);
|
||||
if (insetRect) clearScreenRect(context, insetRect, withOpacity(imageLayerInsetColor, effectiveOpacity));
|
||||
}
|
||||
|
||||
const emptyLayerIds = new Set<string>();
|
||||
@@ -98,9 +104,10 @@ function resolveLayerClipRect(
|
||||
layer: Layer,
|
||||
clipRect: ScreenRect,
|
||||
): ScreenRect | undefined {
|
||||
if (!layer.clippingMask) return clipRect;
|
||||
const layerMask = getLayerMask(layer);
|
||||
if (!layerMask?.enabled) return clipRect;
|
||||
|
||||
const maskBounds = resolveIndexedLayerBounds(documentIndex, layer.clippingMask.maskLayerId);
|
||||
const maskBounds = resolveIndexedLayerBounds(documentIndex, layerMask.maskLayerId);
|
||||
if (!maskBounds) return clipRect;
|
||||
|
||||
return intersectScreenRects(clipRect, documentRectToScreenRect(context.canvas, maskBounds, viewport));
|
||||
@@ -118,6 +125,29 @@ function renderGenerationCandidatePreview(
|
||||
clipRect: ScreenRect,
|
||||
) {
|
||||
const rect = documentRectToScreenRect(context.canvas, generationCandidateBounds(candidate), editor.viewport);
|
||||
const compareMode = editor.generation.compareMode ?? "result";
|
||||
if (compareMode === "before") return;
|
||||
|
||||
if (compareMode === "split") {
|
||||
const splitClipRect = intersectScreenRects(clipRect, {
|
||||
x: rect.x + rect.w / 2,
|
||||
y: rect.y,
|
||||
w: rect.w / 2,
|
||||
h: rect.h,
|
||||
});
|
||||
if (!splitClipRect) return;
|
||||
imageTextureRenderer.render(generationCandidateAsset(candidate), rect, splitClipRect);
|
||||
|
||||
const divider = intersectScreenRects(clipRect, {
|
||||
x: rect.x + rect.w / 2 - 1,
|
||||
y: rect.y,
|
||||
w: 2,
|
||||
h: rect.h,
|
||||
});
|
||||
if (divider) clearScreenRect(context, divider, comparisonDividerColor);
|
||||
return;
|
||||
}
|
||||
|
||||
imageTextureRenderer.render(generationCandidateAsset(candidate), rect, clipRect);
|
||||
}
|
||||
|
||||
@@ -162,3 +192,7 @@ function intersectScreenRects(a: ScreenRect, b: ScreenRect): ScreenRect | undefi
|
||||
|
||||
return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
|
||||
}
|
||||
|
||||
function withOpacity(color: RgbaColor, opacity: number): RgbaColor {
|
||||
return [color[0], color[1], color[2], color[3] * opacity];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user