feat(masks): add mask editing mode

This commit is contained in:
syntaxbullet
2026-07-03 18:07:04 +02:00
parent 4a21b28ed0
commit 15f3a934e0
13 changed files with 211 additions and 20 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<BrushSettings>;
export type ToolSetMaskEditLayerPayload = {
layerId?: LayerId;
};
export const toolSetActiveCommand: Command<ToolSetActivePayload> = {
id: commandIds.toolSetActive,
name: "Set active tool",
@@ -47,6 +52,20 @@ export const toolSetBrushSettingsCommand: Command<ToolSetBrushSettingsPayload> =
},
};
export const toolSetMaskEditLayerCommand: Command<ToolSetMaskEditLayerPayload> = {
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<unknown>[];
export const toolCommands = [toolSetActiveCommand, toolSetBrushSettingsCommand, toolSetMaskEditLayerCommand, toolEnterTemporaryPanCommand, toolExitTemporaryPanCommand] satisfies Command<unknown>[];
function clampNumber(value: number, min: number, max: number) {
if (!Number.isFinite(value)) return min;

View File

@@ -13,6 +13,7 @@ export const initialEditorState: EditorState = {
},
tools: initialToolState,
transformSession: undefined,
maskEditLayerId: undefined,
};
export function createInitialAppState(name = "Untitled"): AppState {

View File

@@ -21,6 +21,7 @@ export type EditorState = {
selection: SelectionState;
tools: ToolState;
transformSession?: TransformSession;
maskEditLayerId?: LayerId;
};
export type HistorySnapshot = {

View File

@@ -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");

View File

@@ -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);

View File

@@ -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);

View File

@@ -77,7 +77,14 @@ export function App({ app }: AppProps) {
dispatch={app.store.dispatch}
/>
</div>
<LayersSheet document={state.document} selection={state.editor.selection} open={layersOpen} dispatch={app.store.dispatch} onClose={() => setLayersOpen(false)} />
<LayersSheet
document={state.document}
selection={state.editor.selection}
maskEditLayerId={state.editor.maskEditLayerId}
open={layersOpen}
dispatch={app.store.dispatch}
onClose={() => setLayersOpen(false)}
/>
<div className="absolute inset-x-0 bottom-4 z-10 flex justify-center">
<BottomControlsIsland
viewport={state.editor.viewport}

View File

@@ -12,12 +12,13 @@ import { downloadArtboardPng } from "./exportArtboardPng";
export type LayersSheetProps = {
document: ImageDocument;
selection: SelectionState;
maskEditLayerId?: string;
open: boolean;
dispatch: AppStore["dispatch"];
onClose: () => 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 (
<div>
<div
@@ -244,6 +249,13 @@ function LayerRow({
<div className="mt-1 flex items-center gap-2 text-xs text-sky-100/60" style={{ paddingLeft: 52 + depth * 16 + maskIndent }}>
<span className="h-px w-5 bg-sky-200/25" />
<span>masked by {maskLayer.name}</span>
<button
type="button"
className={`rounded-full px-2 py-0.5 transition ${editingMask ? "bg-sky-300 text-black" : "bg-sky-400/10 text-sky-100/75 hover:bg-sky-400/20 hover:text-sky-50"}`}
onClick={() => dispatch(commandIds.toolSetMaskEditLayer, { layerId: editingMask ? undefined : layer.clippingMask?.maskLayerId })}
>
{editingMask ? "Editing mask" : "Edit mask"}
</button>
</div>
) : null}
{layer.type === "group"
@@ -259,6 +271,7 @@ function LayerRow({
editingTitle={editingTitle}
setEditingTitle={setEditingTitle}
selectedMaskLayer={selectedMaskLayer}
maskEditLayerId={maskEditLayerId}
dispatch={dispatch}
/>
))

View File

@@ -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,