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>
139 lines
3.9 KiB
TypeScript
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';
|
|
}
|
|
}
|