import { commandIds } from "@commands/ids"; import type { ImageDocument } from "@core/document"; import type { Vec2D } from "@core/geometry"; import type { Layer } from "@core/layer"; import type { RasterLayer } from "@core/raster-layer"; import type { EditorState } from "@editor/state"; import type { AppStore } from "@editor/store"; export type BrushSession = { layerId: string; previousPoint: Vec2D; mode: "brush" | "eraser"; }; export function beginBrushSession(document: ImageDocument, editor: EditorState, point: Vec2D): BrushSession | undefined { if (editor.tools.activeTool !== "brush" && editor.tools.activeTool !== "eraser") return undefined; const layerId = 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; return { layerId, previousPoint: point, mode: editor.tools.activeTool }; } export async function updateBrushSession(options: { store: AppStore; session: BrushSession; point: Vec2D; color: string; size: number; hardness: number; }): Promise { const state = options.store.getState(); const layer = findRasterLayer(state.document.artboards.flatMap((artboard) => artboard.layers), options.session.layerId); if (!layer) return { ...options.session, previousPoint: options.point }; const asset = state.document.assets.find((candidate) => candidate.id === layer.assetId); if (!asset) return { ...options.session, previousPoint: options.point }; const source = await drawStroke({ source: asset.source, width: asset.intrinsicSize.w, 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, size: options.size, hardness: options.hardness, mode: options.session.mode, }); options.store.dispatch(commandIds.documentUpdateAssetSource, { assetId: asset.id, source }); return { ...options.session, previousPoint: options.point }; } function documentPointToAssetPoint(point: Vec2D, layer: RasterLayer, width: number, height: number): Vec2D { return { x: ((point.x - layer.transform.position.x) / Math.max(0.0001, layer.transform.scale.x) / width) * width, y: ((point.y - layer.transform.position.y) / Math.max(0.0001, layer.transform.scale.y) / height) * height, }; } async function drawStroke(options: { source: string; width: number; height: number; from: Vec2D; to: Vec2D; color: string; size: number; hardness: number; mode: "brush" | "eraser"; }) { const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(options.width)); canvas.height = Math.max(1, Math.round(options.height)); const context = canvas.getContext("2d"); if (!context) return options.source; const image = await loadImage(options.source); context.drawImage(image, 0, 0, canvas.width, canvas.height); const hardness = Math.max(0, Math.min(100, options.hardness)) / 100; context.globalCompositeOperation = options.mode === "eraser" ? "destination-out" : "source-over"; context.strokeStyle = options.color; context.shadowColor = options.mode === "eraser" ? "rgba(0,0,0,1)" : options.color; context.shadowBlur = (1 - hardness) * options.size; context.lineWidth = options.size; context.lineCap = "round"; context.lineJoin = "round"; context.beginPath(); context.moveTo(options.from.x, options.from.y); context.lineTo(options.to.x, options.to.y); context.stroke(); context.globalCompositeOperation = "source-over"; return canvas.toDataURL("image/png"); } function loadImage(source: string) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("Failed to load raster layer")); image.src = source; }); } function findRasterLayer(layers: Layer[], layerId: string): RasterLayer | undefined { for (const layer of layers) { if (layer.id === layerId && layer.type === "raster") return layer; if (layer.type === "group") { const child = findRasterLayer(layer.children, layerId); if (child) return child; } } return undefined; }