diff --git a/components/editor/tool-options.tsx b/components/editor/tool-options.tsx
index c244b0b..feb4ef2 100644
--- a/components/editor/tool-options.tsx
+++ b/components/editor/tool-options.tsx
@@ -9,7 +9,7 @@ import { googleFontsLoader } from '@/lib/google-fonts-loader';
import { useCallback } from 'react';
export function ToolOptions() {
- const { activeTool, settings, setSize, setOpacity, setHardness, setColor, setFlow } = useToolStore();
+ const { activeTool, settings, setSize, setOpacity, setHardness, setColor, setFlow, setSecondaryColor, setGradientType } = useToolStore();
const { settings: shapeSettings, setShapeType } = useShapeStore();
const { selectionType, setSelectionType } = useSelectionStore();
const {
@@ -49,6 +49,9 @@ export function ToolOptions() {
// Fill tool
const isFillTool = activeTool === 'fill';
+ // Gradient tool
+ const isGradientTool = activeTool === 'gradient';
+
// Shape tool
const isShapeTool = activeTool === 'shape';
@@ -59,7 +62,7 @@ export function ToolOptions() {
const isTextTool = activeTool === 'text';
// Don't show options bar if no options available
- if (!isDrawingTool && !isFillTool && !isShapeTool && !isSelectionTool && !isTextTool) {
+ if (!isDrawingTool && !isFillTool && !isGradientTool && !isShapeTool && !isSelectionTool && !isTextTool) {
return null;
}
@@ -202,6 +205,79 @@ export function ToolOptions() {
>
)}
+ {/* Gradient Tool Options */}
+ {isGradientTool && (
+ <>
+
+
+
+
+
+
+
+ setColor(e.target.value)}
+ className="h-8 w-16 rounded border border-border cursor-pointer"
+ />
+ setColor(e.target.value)}
+ className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
+ />
+
+
+
+
+ setSecondaryColor(e.target.value)}
+ className="h-8 w-16 rounded border border-border cursor-pointer"
+ />
+ setSecondaryColor(e.target.value)}
+ className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
+ />
+
+
+
+
+ setOpacity(Number(e.target.value) / 100)}
+ className="w-32"
+ />
+
+ {Math.round(settings.opacity * 100)}%
+
+
+ >
+ )}
+
{/* Shape Tool Options */}
{isShapeTool && (
diff --git a/components/tools/tool-palette.tsx b/components/tools/tool-palette.tsx
index 55d2549..342ed0c 100644
--- a/components/tools/tool-palette.tsx
+++ b/components/tools/tool-palette.tsx
@@ -7,6 +7,7 @@ import {
Paintbrush,
Eraser,
PaintBucket,
+ Blend,
MousePointer,
Pipette,
Type,
@@ -21,6 +22,7 @@ const tools: { type: ToolType; icon: React.ReactNode; label: string; shortcut: s
{ type: 'brush', icon:
, label: 'Brush', shortcut: '2' },
{ type: 'eraser', icon:
, label: 'Eraser', shortcut: '3' },
{ type: 'fill', icon:
, label: 'Fill', shortcut: '4' },
+ { type: 'gradient', icon:
, label: 'Gradient (Drag to create)', shortcut: 'G' },
{ type: 'eyedropper', icon:
, label: 'Eyedropper', shortcut: '5' },
{ type: 'text', icon:
, label: 'Text', shortcut: '6' },
{ type: 'select', icon:
, label: 'Select', shortcut: '7' },
diff --git a/lib/tool-loader.ts b/lib/tool-loader.ts
index ed79eaa..dfda827 100644
--- a/lib/tool-loader.ts
+++ b/lib/tool-loader.ts
@@ -47,6 +47,11 @@ async function loadTool(toolKey: string): Promise
{
tool = new FillTool();
break;
}
+ case 'gradient': {
+ const { GradientTool } = await import('@/tools/gradient-tool');
+ tool = new GradientTool();
+ break;
+ }
case 'eyedropper': {
const { EyedropperTool } = await import('@/tools/eyedropper-tool');
tool = new EyedropperTool();
diff --git a/store/tool-store.ts b/store/tool-store.ts
index 575638c..3d69a27 100644
--- a/store/tool-store.ts
+++ b/store/tool-store.ts
@@ -15,6 +15,10 @@ interface ToolStore extends ToolState {
setHardness: (hardness: number) => void;
/** Set color */
setColor: (color: string) => void;
+ /** Set secondary color */
+ setSecondaryColor: (color: string) => void;
+ /** Set gradient type */
+ setGradientType: (type: 'linear' | 'radial' | 'angular') => void;
/** Set flow */
setFlow: (flow: number) => void;
/** Set spacing */
@@ -28,6 +32,8 @@ const DEFAULT_SETTINGS: ToolSettings = {
opacity: 1,
hardness: 1,
color: '#000000',
+ secondaryColor: '#ffffff',
+ gradientType: 'linear',
flow: 1,
spacing: 0.25,
};
@@ -47,6 +53,7 @@ export const useToolStore = create()(
brush: 'crosshair',
eraser: 'crosshair',
fill: 'crosshair',
+ gradient: 'crosshair',
eyedropper: 'crosshair',
text: 'text',
shape: 'crosshair',
@@ -94,6 +101,18 @@ export const useToolStore = create()(
}));
},
+ setSecondaryColor: (color) => {
+ set((state) => ({
+ settings: { ...state.settings, secondaryColor: color },
+ }));
+ },
+
+ setGradientType: (type) => {
+ set((state) => ({
+ settings: { ...state.settings, gradientType: type },
+ }));
+ },
+
setFlow: (flow) => {
set((state) => ({
settings: { ...state.settings, flow: Math.max(0, Math.min(1, flow)) },
diff --git a/tools/gradient-tool.ts b/tools/gradient-tool.ts
new file mode 100644
index 0000000..3f08f48
--- /dev/null
+++ b/tools/gradient-tool.ts
@@ -0,0 +1,138 @@
+import { BaseTool } from './base-tool';
+import type { PointerState, ToolSettings } from '@/types';
+
+export type GradientType = 'linear' | 'radial' | 'angular';
+
+/**
+ * Gradient tool - Linear, Radial, and Angular gradients
+ */
+export class GradientTool extends BaseTool {
+ private startX = 0;
+ private startY = 0;
+ private previewCanvas: HTMLCanvasElement | null = null;
+
+ constructor() {
+ super('Gradient');
+ }
+
+ onPointerDown(
+ pointer: PointerState,
+ ctx: CanvasRenderingContext2D,
+ settings: ToolSettings
+ ): void {
+ this.isDrawing = true;
+ this.startX = pointer.x;
+ this.startY = pointer.y;
+
+ // Create preview canvas
+ this.previewCanvas = document.createElement('canvas');
+ this.previewCanvas.width = ctx.canvas.width;
+ this.previewCanvas.height = ctx.canvas.height;
+ }
+
+ onPointerMove(
+ pointer: PointerState,
+ ctx: CanvasRenderingContext2D,
+ settings: ToolSettings
+ ): void {
+ if (!this.isDrawing || !this.previewCanvas) return;
+
+ const previewCtx = this.previewCanvas.getContext('2d');
+ if (!previewCtx) return;
+
+ // Clear preview
+ previewCtx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
+
+ // Draw gradient preview
+ this.drawGradient(
+ previewCtx,
+ this.startX,
+ this.startY,
+ pointer.x,
+ pointer.y,
+ settings.color,
+ settings.secondaryColor || '#ffffff',
+ settings.gradientType || 'linear'
+ );
+
+ // Draw preview on main canvas (non-destructive)
+ const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ ctx.putImageData(imageData, 0, 0);
+ ctx.globalAlpha = 0.7;
+ ctx.drawImage(this.previewCanvas, 0, 0);
+ ctx.globalAlpha = 1.0;
+ }
+
+ onPointerUp(
+ pointer: PointerState,
+ ctx: CanvasRenderingContext2D,
+ settings: ToolSettings
+ ): void {
+ if (!this.isDrawing) return;
+
+ // Apply final gradient
+ this.drawGradient(
+ ctx,
+ this.startX,
+ this.startY,
+ pointer.x,
+ pointer.y,
+ settings.color,
+ settings.secondaryColor || '#ffffff',
+ settings.gradientType || 'linear'
+ );
+
+ this.isDrawing = false;
+ this.previewCanvas = null;
+ }
+
+ private drawGradient(
+ ctx: CanvasRenderingContext2D,
+ startX: number,
+ startY: number,
+ endX: number,
+ endY: number,
+ color1: string,
+ color2: string,
+ type: GradientType
+ ): void {
+ let gradient: CanvasGradient;
+
+ if (type === 'linear') {
+ gradient = ctx.createLinearGradient(startX, startY, endX, endY);
+ gradient.addColorStop(0, color1);
+ gradient.addColorStop(1, color2);
+ } else if (type === 'radial') {
+ const radius = Math.sqrt(
+ Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)
+ );
+ gradient = ctx.createRadialGradient(startX, startY, 0, startX, startY, radius);
+ gradient.addColorStop(0, color1);
+ gradient.addColorStop(1, color2);
+ } else if (type === 'angular' && 'createConicGradient' in ctx) {
+ // Angular gradient using conic gradient (if supported)
+ const angle = Math.atan2(endY - startY, endX - startX);
+ // @ts-ignore - createConicGradient might not be in all browsers
+ gradient = ctx.createConicGradient(angle, startX, startY);
+ gradient.addColorStop(0, color1);
+ gradient.addColorStop(0.5, color2);
+ gradient.addColorStop(1, color1);
+ } else {
+ // Fallback to radial for angular without conic gradient support
+ const radius = Math.sqrt(
+ Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)
+ );
+ gradient = ctx.createRadialGradient(startX, startY, 0, startX, startY, radius);
+ gradient.addColorStop(0, color1);
+ gradient.addColorStop(1, color2);
+ }
+
+ ctx.fillStyle = gradient;
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+ }
+
+ getCursor(): string {
+ return 'crosshair';
+ }
+}
diff --git a/tools/index.ts b/tools/index.ts
index 447f152..94eff6a 100644
--- a/tools/index.ts
+++ b/tools/index.ts
@@ -3,6 +3,7 @@ export * from './pencil-tool';
export * from './brush-tool';
export * from './eraser-tool';
export * from './fill-tool';
+export * from './gradient-tool';
export * from './eyedropper-tool';
export * from './rectangular-selection-tool';
export * from './elliptical-selection-tool';
diff --git a/types/tool.ts b/types/tool.ts
index d076cd7..161af02 100644
--- a/types/tool.ts
+++ b/types/tool.ts
@@ -10,6 +10,7 @@ export type ToolType =
| 'brush'
| 'eraser'
| 'fill'
+ | 'gradient'
| 'eyedropper'
| 'text'
| 'shape'
@@ -32,6 +33,10 @@ export interface ToolSettings {
hardness: number;
/** Color */
color: string;
+ /** Secondary color (for gradients) */
+ secondaryColor?: string;
+ /** Gradient type */
+ gradientType?: 'linear' | 'radial' | 'angular';
/** Flow rate (0-1) */
flow: number;
/** Spacing between brush stamps */