mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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
89 lines
2.4 KiB
TypeScript
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 };
|
|
}
|