Files
SnapOtter/apps/web/src/components/editor/tools/eraser-tool.tsx
T
SnapOtter 87bbf2f889 feat: add drawing and shape tools for image editor
Implement 8 tool hooks, 7 options bar components, and 1 fill dialog:

Tools:
- Brush: Konva.Line with smooth strokes, hardness via shadowBlur
- Eraser: brush with globalCompositeOperation destination-out
- Shape: Rectangle, Ellipse, Line, Arrow, Polygon, Star with Shift constraint
- Fill (Paint Bucket): scanline flood fill with tolerance and contiguous mode
- Gradient: linear/radial via drag gesture on offscreen canvas
- Clone Stamp: Alt+click source, paint cloned pixels with aligned mode
- Dodge/Burn/Sponge: per-pixel brightness/saturation manipulation by range
- Pixel Brush: blur (box blur), sharpen (unsharp), smudge (directional blend)

Options bars:
- Brush: size/opacity/hardness sliders
- Shape: type selector, fill/stroke color, stroke width, corner radius, sides
- Fill: tolerance slider, contiguous checkbox
- Gradient: linear/radial toggle, opacity, reverse
- Clone Stamp: size/opacity/hardness, aligned toggle
- Dodge/Burn: tool toggle, range dropdown, exposure/flow sliders
- Pixel Brush: tool toggle, size, strength

Common:
- Fill Dialog: Shift+Backspace modal with foreground/background/color/white/black/50% gray
2026-05-06 23:24:34 +08:00

89 lines
2.4 KiB
TypeScript

// apps/web/src/components/editor/tools/eraser-tool.tsx
import type Konva from "konva";
import { useCallback, useRef } from "react";
import { generateId } from "@/lib/utils";
import { useEditorStore } from "@/stores/editor-store";
import type { CanvasObject, LineAttrs } from "@/types/editor";
interface StrokeState {
points: number[];
objectId: string;
}
export function useEraserTool() {
const strokeRef = useRef<StrokeState | null>(null);
const handleMouseDown = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
const stage = e.target.getStage();
if (!stage) return;
const { activeTool, brushSize, brushOpacity, brushHardness, zoom, panOffset } =
useEditorStore.getState();
if (activeTool !== "eraser") return;
const pointer = stage.getPointerPosition();
if (!pointer) return;
const x = (pointer.x - panOffset.x) / zoom;
const y = (pointer.y - panOffset.y) / zoom;
const id = generateId();
const shadowBlurValue = brushSize * 0.4 * (1 - brushHardness);
const attrs: LineAttrs = {
points: [x, y],
stroke: "#000000",
strokeWidth: brushSize,
tension: 0.5,
lineCap: "round",
lineJoin: "round",
opacity: brushOpacity,
globalCompositeOperation: "destination-out",
...(shadowBlurValue > 0 && {
shadowBlur: shadowBlurValue,
shadowColor: "#000000",
shadowOffsetX: 0,
shadowOffsetY: 0,
}),
};
const obj: CanvasObject = {
id,
type: "line",
layerId: useEditorStore.getState().activeLayerId,
attrs,
};
useEditorStore.getState().addObject(obj);
strokeRef.current = { points: [x, y], objectId: id };
}, []);
const handleMouseMove = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
if (!strokeRef.current) return;
const stage = e.target.getStage();
if (!stage) return;
const { zoom, panOffset } = useEditorStore.getState();
const pointer = stage.getPointerPosition();
if (!pointer) return;
const x = (pointer.x - panOffset.x) / zoom;
const y = (pointer.y - panOffset.y) / zoom;
strokeRef.current.points = [...strokeRef.current.points, x, y];
useEditorStore.getState().updateObject(strokeRef.current.objectId, {
points: [...strokeRef.current.points],
});
}, []);
const handleMouseUp = useCallback(() => {
strokeRef.current = null;
}, []);
return { handleMouseDown, handleMouseMove, handleMouseUp };
}