Files
paint-ui/tools/gradient-tool.ts
Sebastian Krüger 8f595ac6c4 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>
2025-11-21 19:48:00 +01:00

139 lines
3.9 KiB
TypeScript

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';
}
}