108 lines
2.6 KiB
TypeScript
108 lines
2.6 KiB
TypeScript
|
|
import { BaseTool } from './base-tool';
|
||
|
|
import type { PointerState, ToolSettings } from '@/types';
|
||
|
|
import { distance } from '@/lib/utils';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Brush tool - Variable size and opacity with smooth strokes
|
||
|
|
*/
|
||
|
|
export class BrushTool extends BaseTool {
|
||
|
|
private lastX = 0;
|
||
|
|
private lastY = 0;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super('Brush');
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerDown(
|
||
|
|
pointer: PointerState,
|
||
|
|
ctx: CanvasRenderingContext2D,
|
||
|
|
settings: ToolSettings
|
||
|
|
): void {
|
||
|
|
this.isDrawing = true;
|
||
|
|
this.lastX = pointer.x;
|
||
|
|
this.lastY = pointer.y;
|
||
|
|
|
||
|
|
// Draw initial stamp
|
||
|
|
this.drawStamp(pointer.x, pointer.y, ctx, settings);
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerMove(
|
||
|
|
pointer: PointerState,
|
||
|
|
ctx: CanvasRenderingContext2D,
|
||
|
|
settings: ToolSettings
|
||
|
|
): void {
|
||
|
|
if (!this.isDrawing) return;
|
||
|
|
|
||
|
|
// Calculate distance from last point
|
||
|
|
const dist = distance(this.lastX, this.lastY, pointer.x, pointer.y);
|
||
|
|
const spacing = settings.size * settings.spacing;
|
||
|
|
|
||
|
|
if (dist >= spacing) {
|
||
|
|
// Interpolate between points for smooth stroke
|
||
|
|
const steps = Math.ceil(dist / spacing);
|
||
|
|
|
||
|
|
for (let i = 1; i <= steps; i++) {
|
||
|
|
const t = i / steps;
|
||
|
|
const x = this.lastX + (pointer.x - this.lastX) * t;
|
||
|
|
const y = this.lastY + (pointer.y - this.lastY) * t;
|
||
|
|
|
||
|
|
this.drawStamp(x, y, ctx, settings);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.lastX = pointer.x;
|
||
|
|
this.lastY = pointer.y;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerUp(
|
||
|
|
pointer: PointerState,
|
||
|
|
ctx: CanvasRenderingContext2D,
|
||
|
|
settings: ToolSettings
|
||
|
|
): void {
|
||
|
|
this.isDrawing = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw a single brush stamp
|
||
|
|
*/
|
||
|
|
private drawStamp(
|
||
|
|
x: number,
|
||
|
|
y: number,
|
||
|
|
ctx: CanvasRenderingContext2D,
|
||
|
|
settings: ToolSettings
|
||
|
|
): void {
|
||
|
|
const size = settings.size;
|
||
|
|
const hardness = settings.hardness;
|
||
|
|
const opacity = settings.opacity * settings.flow;
|
||
|
|
|
||
|
|
// Create radial gradient for soft brush
|
||
|
|
const gradient = ctx.createRadialGradient(x, y, 0, x, y, size / 2);
|
||
|
|
|
||
|
|
// Parse color to add alpha
|
||
|
|
const color = settings.color;
|
||
|
|
|
||
|
|
if (hardness >= 1) {
|
||
|
|
// Hard brush
|
||
|
|
gradient.addColorStop(0, color);
|
||
|
|
gradient.addColorStop(1, color);
|
||
|
|
} else {
|
||
|
|
// Soft brush with hardness
|
||
|
|
gradient.addColorStop(0, color);
|
||
|
|
gradient.addColorStop(hardness, color);
|
||
|
|
gradient.addColorStop(1, color.replace('rgb', 'rgba').replace(')', ', 0)'));
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.save();
|
||
|
|
ctx.globalAlpha = opacity;
|
||
|
|
ctx.fillStyle = gradient;
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
||
|
|
ctx.fill();
|
||
|
|
ctx.restore();
|
||
|
|
}
|
||
|
|
|
||
|
|
getCursor(settings: ToolSettings): string {
|
||
|
|
return 'crosshair';
|
||
|
|
}
|
||
|
|
}
|