feat(tools): add eraser tool
This commit is contained in:
@@ -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];
|
export type ToolId = (typeof availableToolIds)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -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 { commandIds } from "@commands/ids";
|
||||||
import type { AppStore } from "@editor/store";
|
import type { AppStore } from "@editor/store";
|
||||||
import type { InteractionMode, ToolId } from "@editor/tools";
|
import type { InteractionMode, ToolId } from "@editor/tools";
|
||||||
@@ -45,6 +45,8 @@ function iconForTool(tool: ToolId) {
|
|||||||
return Crop;
|
return Crop;
|
||||||
case "brush":
|
case "brush":
|
||||||
return PaintBrush;
|
return PaintBrush;
|
||||||
|
case "eraser":
|
||||||
|
return Eraser;
|
||||||
case "pan":
|
case "pan":
|
||||||
return Hand;
|
return Hand;
|
||||||
case "select":
|
case "select":
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import type { AppStore } from "@editor/store";
|
|||||||
export type BrushSession = {
|
export type BrushSession = {
|
||||||
layerId: string;
|
layerId: string;
|
||||||
previousPoint: Vec2D;
|
previousPoint: Vec2D;
|
||||||
|
mode: "brush" | "eraser";
|
||||||
};
|
};
|
||||||
|
|
||||||
export function beginBrushSession(document: ImageDocument, editor: EditorState, point: Vec2D): BrushSession | undefined {
|
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];
|
const layerId = editor.selection.layerIds[0];
|
||||||
if (!layerId) return undefined;
|
if (!layerId) return undefined;
|
||||||
const layer = findRasterLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
|
const layer = findRasterLayer(document.artboards.flatMap((artboard) => artboard.layers), layerId);
|
||||||
if (!layer || layer.locked || !layer.visible) return undefined;
|
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: {
|
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),
|
to: documentPointToAssetPoint(options.point, layer, asset.intrinsicSize.w, asset.intrinsicSize.h),
|
||||||
color: options.color ?? "#111827",
|
color: options.color ?? "#111827",
|
||||||
size: options.size ?? 8,
|
size: options.size ?? 8,
|
||||||
|
mode: options.session.mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
options.store.dispatch(commandIds.documentUpdateAssetSource, { assetId: asset.id, source });
|
options.store.dispatch(commandIds.documentUpdateAssetSource, { assetId: asset.id, source });
|
||||||
@@ -63,6 +65,7 @@ async function drawStroke(options: {
|
|||||||
to: Vec2D;
|
to: Vec2D;
|
||||||
color: string;
|
color: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
mode: "brush" | "eraser";
|
||||||
}) {
|
}) {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = Math.max(1, Math.round(options.width));
|
canvas.width = Math.max(1, Math.round(options.width));
|
||||||
@@ -72,6 +75,7 @@ async function drawStroke(options: {
|
|||||||
|
|
||||||
const image = await loadImage(options.source);
|
const image = await loadImage(options.source);
|
||||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
context.globalCompositeOperation = options.mode === "eraser" ? "destination-out" : "source-over";
|
||||||
context.strokeStyle = options.color;
|
context.strokeStyle = options.color;
|
||||||
context.lineWidth = options.size;
|
context.lineWidth = options.size;
|
||||||
context.lineCap = "round";
|
context.lineCap = "round";
|
||||||
@@ -80,6 +84,7 @@ async function drawStroke(options: {
|
|||||||
context.moveTo(options.from.x, options.from.y);
|
context.moveTo(options.from.x, options.from.y);
|
||||||
context.lineTo(options.to.x, options.to.y);
|
context.lineTo(options.to.x, options.to.y);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
|
context.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
return canvas.toDataURL("image/png");
|
return canvas.toDataURL("image/png");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import type { CanvasInputState } from "./useCanvasInput";
|
|||||||
export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) {
|
export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) {
|
||||||
if (input.isPanning) return "cursor-grabbing";
|
if (input.isPanning) return "cursor-grabbing";
|
||||||
if (isPanInteractionMode(interactionMode)) return "cursor-grab";
|
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";
|
return "cursor-default";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export function labelForTool(tool: ToolId): string {
|
|||||||
return "Crop";
|
return "Crop";
|
||||||
case "brush":
|
case "brush":
|
||||||
return "Brush";
|
return "Brush";
|
||||||
|
case "eraser":
|
||||||
|
return "Eraser";
|
||||||
case "pan":
|
case "pan":
|
||||||
return "Pan";
|
return "Pan";
|
||||||
case "select":
|
case "select":
|
||||||
|
|||||||
Reference in New Issue
Block a user