116 lines
2.8 KiB
TypeScript
116 lines
2.8 KiB
TypeScript
|
|
import { BaseTool } from './base-tool';
|
||
|
|
import type { PointerState, ToolSettings } from '@/types';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fill tool - Flood fill algorithm
|
||
|
|
*/
|
||
|
|
export class FillTool extends BaseTool {
|
||
|
|
constructor() {
|
||
|
|
super('Fill');
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerDown(
|
||
|
|
pointer: PointerState,
|
||
|
|
ctx: CanvasRenderingContext2D,
|
||
|
|
settings: ToolSettings
|
||
|
|
): void {
|
||
|
|
const x = Math.floor(pointer.x);
|
||
|
|
const y = Math.floor(pointer.y);
|
||
|
|
|
||
|
|
this.floodFill(x, y, settings.color, ctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerMove(): void {
|
||
|
|
// No-op for fill tool
|
||
|
|
}
|
||
|
|
|
||
|
|
onPointerUp(): void {
|
||
|
|
// No-op for fill tool
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Flood fill implementation using scanline algorithm
|
||
|
|
*/
|
||
|
|
private floodFill(
|
||
|
|
startX: number,
|
||
|
|
startY: number,
|
||
|
|
fillColor: string,
|
||
|
|
ctx: CanvasRenderingContext2D
|
||
|
|
): void {
|
||
|
|
const canvas = ctx.canvas;
|
||
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||
|
|
const data = imageData.data;
|
||
|
|
|
||
|
|
// Get target color at start position
|
||
|
|
const startPos = (startY * canvas.width + startX) * 4;
|
||
|
|
const targetR = data[startPos];
|
||
|
|
const targetG = data[startPos + 1];
|
||
|
|
const targetB = data[startPos + 2];
|
||
|
|
const targetA = data[startPos + 3];
|
||
|
|
|
||
|
|
// Convert fill color to RGBA
|
||
|
|
const tempCanvas = document.createElement('canvas');
|
||
|
|
tempCanvas.width = 1;
|
||
|
|
tempCanvas.height = 1;
|
||
|
|
const tempCtx = tempCanvas.getContext('2d')!;
|
||
|
|
tempCtx.fillStyle = fillColor;
|
||
|
|
tempCtx.fillRect(0, 0, 1, 1);
|
||
|
|
const fillData = tempCtx.getImageData(0, 0, 1, 1).data;
|
||
|
|
const fillR = fillData[0];
|
||
|
|
const fillG = fillData[1];
|
||
|
|
const fillB = fillData[2];
|
||
|
|
const fillA = fillData[3];
|
||
|
|
|
||
|
|
// Check if target and fill colors are the same
|
||
|
|
if (
|
||
|
|
targetR === fillR &&
|
||
|
|
targetG === fillG &&
|
||
|
|
targetB === fillB &&
|
||
|
|
targetA === fillA
|
||
|
|
) {
|
||
|
|
return; // No need to fill
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scanline flood fill
|
||
|
|
const stack: [number, number][] = [[startX, startY]];
|
||
|
|
const visited = new Set<string>();
|
||
|
|
|
||
|
|
while (stack.length > 0) {
|
||
|
|
const [x, y] = stack.pop()!;
|
||
|
|
|
||
|
|
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) continue;
|
||
|
|
|
||
|
|
const key = `${x},${y}`;
|
||
|
|
if (visited.has(key)) continue;
|
||
|
|
visited.add(key);
|
||
|
|
|
||
|
|
const pos = (y * canvas.width + x) * 4;
|
||
|
|
|
||
|
|
// Check if pixel matches target color
|
||
|
|
if (
|
||
|
|
data[pos] !== targetR ||
|
||
|
|
data[pos + 1] !== targetG ||
|
||
|
|
data[pos + 2] !== targetB ||
|
||
|
|
data[pos + 3] !== targetA
|
||
|
|
) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fill pixel
|
||
|
|
data[pos] = fillR;
|
||
|
|
data[pos + 1] = fillG;
|
||
|
|
data[pos + 2] = fillB;
|
||
|
|
data[pos + 3] = fillA;
|
||
|
|
|
||
|
|
// Add neighbors to stack
|
||
|
|
stack.push([x + 1, y]);
|
||
|
|
stack.push([x - 1, y]);
|
||
|
|
stack.push([x, y + 1]);
|
||
|
|
stack.push([x, y - 1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Put modified image data back
|
||
|
|
ctx.putImageData(imageData, 0, 0);
|
||
|
|
}
|
||
|
|
}
|