diff --git a/commands/ids.ts b/commands/ids.ts index e043654..2fa95b0 100644 --- a/commands/ids.ts +++ b/commands/ids.ts @@ -23,6 +23,7 @@ export const commandIds = { selectionAddLayer: "selection.addLayer", toolSetActive: "tool.setActive", toolSetBrushSettings: "tool.setBrushSettings", + toolSetMaskEditLayer: "tool.setMaskEditLayer", toolEnterTemporaryPan: "tool.enterTemporaryPan", toolExitTemporaryPan: "tool.exitTemporaryPan", transformBegin: "transform.begin", diff --git a/commands/index.ts b/commands/index.ts index b8dcd39..163d278 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -50,10 +50,10 @@ export type { CommandRegistry } from "./registry"; export { createCommandRegistry } from "./registry"; export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection"; export type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection"; -export { toolCommands, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand } from "./tool"; +export { toolCommands, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetMaskEditLayerCommand } from "./tool"; export { transformBeginCommand, transformCommands, transformEndCommand, transformSetBoundsCommand, transformUpdateCommand } from "./transform"; export type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform"; -export type { ToolSetActivePayload, ToolSetBrushSettingsPayload } from "./tool"; +export type { ToolSetActivePayload, ToolSetBrushSettingsPayload, ToolSetMaskEditLayerPayload } from "./tool"; export { viewportCommands, viewportPanCommand, diff --git a/commands/payloads.ts b/commands/payloads.ts index 5a5a4f7..86fca41 100644 --- a/commands/payloads.ts +++ b/commands/payloads.ts @@ -21,7 +21,7 @@ import type { DocumentUngroupLayerPayload, } from "./document"; import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection"; -import type { ToolSetActivePayload, ToolSetBrushSettingsPayload } from "./tool"; +import type { ToolSetActivePayload, ToolSetBrushSettingsPayload, ToolSetMaskEditLayerPayload } from "./tool"; import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform"; import type { ViewportFitArtboardPayload, @@ -56,6 +56,7 @@ export type CommandPayloads = { [commandIds.selectionAddLayer]: SelectionAddLayerPayload; [commandIds.toolSetActive]: ToolSetActivePayload; [commandIds.toolSetBrushSettings]: ToolSetBrushSettingsPayload; + [commandIds.toolSetMaskEditLayer]: ToolSetMaskEditLayerPayload; [commandIds.toolEnterTemporaryPan]: void; [commandIds.toolExitTemporaryPan]: void; [commandIds.transformBegin]: TransformBeginPayload; diff --git a/commands/tool.test.ts b/commands/tool.test.ts index c81f317..6e67729 100644 --- a/commands/tool.test.ts +++ b/commands/tool.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { createInitialAppState } from "@editor/initial-state"; -import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand } from "./tool"; +import { toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand, toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetMaskEditLayerCommand } from "./tool"; describe("tool commands", () => { test("sets active tool", () => { @@ -14,6 +14,12 @@ describe("tool commands", () => { expect(next.editor.tools.brush).toEqual({ color: "#ff0000", size: 24, hardness: 50 }); }); + test("sets mask edit layer", () => { + const next = toolSetMaskEditLayerCommand.execute({ state: createInitialAppState("Test") }, { layerId: "mask-1" }); + + expect(next.editor.maskEditLayerId).toBe("mask-1"); + }); + test("enters and exits temporary pan", () => { const initial = createInitialAppState("Test"); const panning = toolEnterTemporaryPanCommand.execute({ state: initial }, undefined); diff --git a/commands/tool.ts b/commands/tool.ts index d90f2f2..371d609 100644 --- a/commands/tool.ts +++ b/commands/tool.ts @@ -1,3 +1,4 @@ +import type { LayerId } from "@core/id"; import type { BrushSettings, ToolId } from "@editor/tools"; import type { Command } from "./command"; import { commandIds } from "./ids"; @@ -8,6 +9,10 @@ export type ToolSetActivePayload = { export type ToolSetBrushSettingsPayload = Partial; +export type ToolSetMaskEditLayerPayload = { + layerId?: LayerId; +}; + export const toolSetActiveCommand: Command = { id: commandIds.toolSetActive, name: "Set active tool", @@ -47,6 +52,20 @@ export const toolSetBrushSettingsCommand: Command = }, }; +export const toolSetMaskEditLayerCommand: Command = { + id: commandIds.toolSetMaskEditLayer, + name: "Set mask edit layer", + execute({ state }, payload) { + return { + ...state, + editor: { + ...state.editor, + maskEditLayerId: payload.layerId, + }, + }; + }, +}; + export const toolEnterTemporaryPanCommand: Command = { id: commandIds.toolEnterTemporaryPan, name: "Enter temporary pan", @@ -87,7 +106,7 @@ export const toolExitTemporaryPanCommand: Command = { }, }; -export const toolCommands = [toolSetActiveCommand, toolSetBrushSettingsCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command[]; +export const toolCommands = [toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetMaskEditLayerCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command[]; function clampNumber(value: number, min: number, max: number) { if (!Number.isFinite(value)) return min; diff --git a/editor/initial-state.ts b/editor/initial-state.ts index 7d58ebc..fffd74f 100644 --- a/editor/initial-state.ts +++ b/editor/initial-state.ts @@ -13,6 +13,7 @@ export const initialEditorState: EditorState = { }, tools: initialToolState, transformSession: undefined, + maskEditLayerId: undefined, }; export function createInitialAppState(name = "Untitled"): AppState { diff --git a/editor/state.ts b/editor/state.ts index a2bad84..b5e3f35 100644 --- a/editor/state.ts +++ b/editor/state.ts @@ -21,6 +21,7 @@ export type EditorState = { selection: SelectionState; tools: ToolState; transformSession?: TransformSession; + maskEditLayerId?: LayerId; }; export type HistorySnapshot = { diff --git a/renderer/image-textures.ts b/renderer/image-textures.ts index c508077..475336a 100644 --- a/renderer/image-textures.ts +++ b/renderer/image-textures.ts @@ -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(); 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"); diff --git a/renderer/layers.ts b/renderer/layers.ts index 4cc10e4..97f547a 100644 --- a/renderer/layers.ts +++ b/renderer/layers.ts @@ -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, ) { - 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()) { for (const layer of layers) { if (layer.clippingMask) ids.add(layer.clippingMask.maskLayerId); diff --git a/renderer/renderer.ts b/renderer/renderer.ts index db08214..24f9438 100644 --- a/renderer/renderer.ts +++ b/renderer/renderer.ts @@ -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); diff --git a/view/App.tsx b/view/App.tsx index 459d7d7..63e3c5d 100644 --- a/view/App.tsx +++ b/view/App.tsx @@ -77,7 +77,14 @@ export function App({ app }: AppProps) { dispatch={app.store.dispatch} /> - setLayersOpen(false)} /> + setLayersOpen(false)} + />
void; }; -export function LayersSheet({ document, selection, open, dispatch, onClose }: LayersSheetProps) { +export function LayersSheet({ document, selection, maskEditLayerId, open, dispatch, onClose }: LayersSheetProps) { const selectedArtboardId = selection.artboardId ?? document.artboards[0]?.id; const selectedLayer = findLayerInfoInDocument(document, selection.layerIds[0]); const canGroup = Boolean(selection.artboardId && selection.layerIds.length > 0); @@ -137,6 +138,7 @@ export function LayersSheet({ document, selection, open, dispatch, onClose }: La editingTitle={editingTitle} setEditingTitle={setEditingTitle} selectedMaskLayer={selectedLayer} + maskEditLayerId={maskEditLayerId} dispatch={dispatch} /> )) @@ -159,6 +161,7 @@ function LayerRow({ editingTitle, setEditingTitle, selectedMaskLayer, + maskEditLayerId, dispatch, }: { document: ImageDocument; @@ -170,6 +173,7 @@ function LayerRow({ editingTitle: EditingTitle | undefined; setEditingTitle: (editingTitle: EditingTitle | undefined) => void; selectedMaskLayer?: LayerInfo; + maskEditLayerId?: string; dispatch: AppStore["dispatch"]; }) { const selected = selectedLayerIds.includes(layer.id); @@ -178,6 +182,7 @@ function LayerRow({ const maskIndent = maskLayer ? 24 : 0; const canSetMask = Boolean(selectedMaskLayer && layerInfo && selectedMaskLayer.layer.id !== layer.id && selectedMaskLayer.artboardId === layerInfo.artboardId && selectedMaskLayer.parentGroupId === layerInfo.parentGroupId); + const editingMask = maskEditLayerId === layer.clippingMask?.maskLayerId; return (
masked by {maskLayer.name} +
) : null} {layer.type === "group" @@ -259,6 +271,7 @@ function LayerRow({ editingTitle={editingTitle} setEditingTitle={setEditingTitle} selectedMaskLayer={selectedMaskLayer} + maskEditLayerId={maskEditLayerId} dispatch={dispatch} /> )) diff --git a/view/canvas/brush.ts b/view/canvas/brush.ts index 36c58af..feda50e 100644 --- a/view/canvas/brush.ts +++ b/view/canvas/brush.ts @@ -15,7 +15,7 @@ export type BrushSession = { export function beginBrushSession(document: ImageDocument, editor: EditorState, point: Vec2D): BrushSession | undefined { if (isPanInteractionMode(editor.tools.interactionMode) || (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser")) return undefined; - const layerId = editor.selection.layerIds[0]; + const layerId = editor.maskEditLayerId ?? editor.selection.layerIds[0]; if (!layerId) return undefined; const layer = findRasterLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId); if (!layer || layer.locked || !layer.visible) return undefined; @@ -43,7 +43,7 @@ export async function updateBrushSession(options: { height: asset.intrinsicSize.h, from: documentPointToAssetPoint(options.session.previousPoint, layer, asset.intrinsicSize.w, asset.intrinsicSize.h), to: documentPointToAssetPoint(options.point, layer, asset.intrinsicSize.w, asset.intrinsicSize.h), - color: options.color, + color: state.editor.maskEditLayerId ? "#ffffff" : options.color, size: options.size, hardness: options.hardness, mode: options.session.mode,