diff --git a/editor/tools.ts b/editor/tools.ts index f0c2416..aece09c 100644 --- a/editor/tools.ts +++ b/editor/tools.ts @@ -1,4 +1,4 @@ -export const availableToolIds = ["select", "crop", "brush", "pan"] as const; +export const availableToolIds = ["select", "crop", "brush", "eraser", "pan"] as const; export type ToolId = (typeof availableToolIds)[number]; diff --git a/view/ToolOverlay.tsx b/view/ToolOverlay.tsx index d694165..9aa79bc 100644 --- a/view/ToolOverlay.tsx +++ b/view/ToolOverlay.tsx @@ -1,4 +1,4 @@ -import { Crop, Cursor, Hand, PaintBrush } from "@phosphor-icons/react"; +import { Crop, Cursor, Eraser, Hand, PaintBrush } from "@phosphor-icons/react"; import { commandIds } from "@commands/ids"; import type { AppStore } from "@editor/store"; import type { InteractionMode, ToolId } from "@editor/tools"; @@ -45,6 +45,8 @@ function iconForTool(tool: ToolId) { return Crop; case "brush": return PaintBrush; + case "eraser": + return Eraser; case "pan": return Hand; case "select": diff --git a/view/canvas/brush.ts b/view/canvas/brush.ts index dba8b27..28ea555 100644 --- a/view/canvas/brush.ts +++ b/view/canvas/brush.ts @@ -9,15 +9,16 @@ 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") return 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 }; + return { layerId, previousPoint: point, mode: editor.tools.activeTool }; } export async function updateBrushSession(options: { @@ -42,6 +43,7 @@ export async function updateBrushSession(options: { to: documentPointToAssetPoint(options.point, layer, asset.intrinsicSize.w, asset.intrinsicSize.h), color: options.color ?? "#111827", size: options.size ?? 8, + mode: options.session.mode, }); options.store.dispatch(commandIds.documentUpdateAssetSource, { assetId: asset.id, source }); @@ -63,6 +65,7 @@ async function drawStroke(options: { to: Vec2D; color: string; size: number; + mode: "brush" | "eraser"; }) { const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(options.width)); @@ -72,6 +75,7 @@ async function drawStroke(options: { const image = await loadImage(options.source); context.drawImage(image, 0, 0, canvas.width, canvas.height); + context.globalCompositeOperation = options.mode === "eraser" ? "destination-out" : "source-over"; context.strokeStyle = options.color; context.lineWidth = options.size; context.lineCap = "round"; @@ -80,6 +84,7 @@ async function drawStroke(options: { 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"); } diff --git a/view/canvas/cursor.ts b/view/canvas/cursor.ts index abf9a42..611f902 100644 --- a/view/canvas/cursor.ts +++ b/view/canvas/cursor.ts @@ -5,6 +5,6 @@ import type { CanvasInputState } from "./useCanvasInput"; export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) { if (input.isPanning) return "cursor-grabbing"; if (isPanInteractionMode(interactionMode)) return "cursor-grab"; - if (interactionMode.type === "tool" && (interactionMode.tool === "crop" || interactionMode.tool === "brush")) return "cursor-crosshair"; + if (interactionMode.type === "tool" && (interactionMode.tool === "crop" || interactionMode.tool === "brush" || interactionMode.tool === "eraser")) return "cursor-crosshair"; return "cursor-default"; } diff --git a/view/toolLabels.ts b/view/toolLabels.ts index a27872f..114acb5 100644 --- a/view/toolLabels.ts +++ b/view/toolLabels.ts @@ -6,6 +6,8 @@ export function labelForTool(tool: ToolId): string { return "Crop"; case "brush": return "Brush"; + case "eraser": + return "Eraser"; case "pan": return "Pan"; case "select":