feat(phase-13): implement gradient tool with linear, radial, and angular modes

Add comprehensive gradient tool with three gradient types and full UI integration.

Features:
- Gradient tool with drag-to-create interaction
- Three gradient types: Linear, Radial, and Angular (conic)
- Live preview during drag with 70% opacity overlay
- Primary and secondary color selection
- Gradient type selector in tool options
- Undo/redo support through command system
- Fallback to radial gradient for browsers without conic gradient support

Changes:
- Created tools/gradient-tool.ts with GradientTool class
- Added 'gradient' to ToolType in types/tool.ts
- Extended ToolSettings with secondaryColor and gradientType
- Updated store/tool-store.ts with setSecondaryColor and setGradientType methods
- Added gradient tool loading in lib/tool-loader.ts
- Added gradient button to tool palette with 'G' shortcut
- Added gradient tool options UI in components/editor/tool-options.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 19:48:00 +01:00
parent c97ec454f7
commit 8f595ac6c4
7 changed files with 248 additions and 2 deletions

View File

@@ -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 && (
<>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Type:
</label>
<select
value={settings.gradientType || 'linear'}
onChange={(e) => setGradientType(e.target.value as 'linear' | 'radial' | 'angular')}
className="px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground"
>
<option value="linear">Linear</option>
<option value="radial">Radial</option>
<option value="angular">Angular</option>
</select>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Start Color:
</label>
<input
type="color"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="h-8 w-16 rounded border border-border cursor-pointer"
/>
<input
type="text"
value={settings.color}
onChange={(e) => setColor(e.target.value)}
className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
End Color:
</label>
<input
type="color"
value={settings.secondaryColor || '#ffffff'}
onChange={(e) => setSecondaryColor(e.target.value)}
className="h-8 w-16 rounded border border-border cursor-pointer"
/>
<input
type="text"
value={settings.secondaryColor || '#ffffff'}
onChange={(e) => setSecondaryColor(e.target.value)}
className="w-24 px-2 py-1 text-xs rounded border border-border bg-background text-foreground"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-card-foreground whitespace-nowrap">
Opacity:
</label>
<input
type="range"
min="0"
max="100"
value={settings.opacity * 100}
onChange={(e) => setOpacity(Number(e.target.value) / 100)}
className="w-32"
/>
<span className="text-sm text-muted-foreground w-10">
{Math.round(settings.opacity * 100)}%
</span>
</div>
</>
)}
{/* Shape Tool Options */}
{isShapeTool && (
<div className="flex items-center gap-2">

View File

@@ -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: <Paintbrush className="h-5 w-5" />, label: 'Brush', shortcut: '2' },
{ type: 'eraser', icon: <Eraser className="h-5 w-5" />, label: 'Eraser', shortcut: '3' },
{ type: 'fill', icon: <PaintBucket className="h-5 w-5" />, label: 'Fill', shortcut: '4' },
{ type: 'gradient', icon: <Blend className="h-5 w-5" />, label: 'Gradient (Drag to create)', shortcut: 'G' },
{ type: 'eyedropper', icon: <Pipette className="h-5 w-5" />, label: 'Eyedropper', shortcut: '5' },
{ type: 'text', icon: <Type className="h-5 w-5" />, label: 'Text', shortcut: '6' },
{ type: 'select', icon: <MousePointer className="h-5 w-5" />, label: 'Select', shortcut: '7' },