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
142 lines
3.7 KiB
TypeScript
142 lines
3.7 KiB
TypeScript
// apps/web/src/components/editor/tools/gradient-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 } from "@/types/editor";
|
|
|
|
export type GradientType = "linear" | "radial";
|
|
|
|
let gradientType: GradientType = "linear";
|
|
let gradientOpacity = 1;
|
|
let gradientReverse = false;
|
|
|
|
export function setGradientType(type: GradientType) {
|
|
gradientType = type;
|
|
}
|
|
|
|
export function getGradientType(): GradientType {
|
|
return gradientType;
|
|
}
|
|
|
|
export function setGradientOpacity(value: number) {
|
|
gradientOpacity = Math.max(0, Math.min(1, value));
|
|
}
|
|
|
|
export function getGradientOpacity(): number {
|
|
return gradientOpacity;
|
|
}
|
|
|
|
export function setGradientReverse(value: boolean) {
|
|
gradientReverse = value;
|
|
}
|
|
|
|
export function getGradientReverse(): boolean {
|
|
return gradientReverse;
|
|
}
|
|
|
|
interface DragState {
|
|
startX: number;
|
|
startY: number;
|
|
}
|
|
|
|
export function useGradientTool() {
|
|
const dragRef = useRef<DragState | null>(null);
|
|
|
|
const handleMouseDown = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
const { activeTool, zoom, panOffset } = useEditorStore.getState();
|
|
|
|
if (activeTool !== "gradient") return;
|
|
|
|
const stage = e.target.getStage();
|
|
if (!stage) return;
|
|
|
|
const pointer = stage.getPointerPosition();
|
|
if (!pointer) return;
|
|
|
|
const x = (pointer.x - panOffset.x) / zoom;
|
|
const y = (pointer.y - panOffset.y) / zoom;
|
|
|
|
dragRef.current = { startX: x, startY: y };
|
|
}, []);
|
|
|
|
const handleMouseMove = useCallback((_e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
// Could show a preview line/circle here; keeping simple for now
|
|
}, []);
|
|
|
|
const handleMouseUp = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
|
|
if (!dragRef.current) return;
|
|
|
|
const { foregroundColor, backgroundColor, canvasSize, zoom, panOffset } =
|
|
useEditorStore.getState();
|
|
|
|
const stage = e.target.getStage();
|
|
if (!stage) return;
|
|
|
|
const pointer = stage.getPointerPosition();
|
|
if (!pointer) return;
|
|
|
|
const endX = (pointer.x - panOffset.x) / zoom;
|
|
const endY = (pointer.y - panOffset.y) / zoom;
|
|
const { startX, startY } = dragRef.current;
|
|
|
|
const dx = endX - startX;
|
|
const dy = endY - startY;
|
|
if (Math.sqrt(dx * dx + dy * dy) < 2) {
|
|
dragRef.current = null;
|
|
return;
|
|
}
|
|
|
|
// Build gradient on an offscreen canvas
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = canvasSize.width;
|
|
canvas.height = canvasSize.height;
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) {
|
|
dragRef.current = null;
|
|
return;
|
|
}
|
|
|
|
const color1 = gradientReverse ? backgroundColor : foregroundColor;
|
|
const color2 = gradientReverse ? foregroundColor : backgroundColor;
|
|
|
|
let gradient: CanvasGradient;
|
|
|
|
if (gradientType === "radial") {
|
|
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
gradient = ctx.createRadialGradient(startX, startY, 0, startX, startY, radius);
|
|
} else {
|
|
gradient = ctx.createLinearGradient(startX, startY, endX, endY);
|
|
}
|
|
|
|
gradient.addColorStop(0, color1);
|
|
gradient.addColorStop(1, color2);
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
|
|
|
|
const dataUrl = canvas.toDataURL();
|
|
|
|
const obj: CanvasObject = {
|
|
id: generateId(),
|
|
type: "image",
|
|
layerId: useEditorStore.getState().activeLayerId,
|
|
attrs: {
|
|
x: 0,
|
|
y: 0,
|
|
width: canvasSize.width,
|
|
height: canvasSize.height,
|
|
rotation: 0,
|
|
opacity: gradientOpacity,
|
|
src: dataUrl,
|
|
},
|
|
};
|
|
|
|
useEditorStore.getState().addObject(obj);
|
|
dragRef.current = null;
|
|
}, []);
|
|
|
|
return { handleMouseDown, handleMouseMove, handleMouseUp };
|
|
}
|