288 lines
8.0 KiB
TypeScript
288 lines
8.0 KiB
TypeScript
|
|
import type { Layer, Selection } from '@/types';
|
||
|
|
import { useLayerStore } from '@/store/layer-store';
|
||
|
|
import { useSelectionStore } from '@/store/selection-store';
|
||
|
|
import { useHistoryStore } from '@/store/history-store';
|
||
|
|
import { DrawCommand } from '@/core/commands/draw-command';
|
||
|
|
import { cloneCanvas } from './canvas-utils';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Copy selected pixels to clipboard (as canvas)
|
||
|
|
*/
|
||
|
|
export function copySelection(): HTMLCanvasElement | null {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
|
||
|
|
if (!activeSelection) return null;
|
||
|
|
|
||
|
|
const layer = layers.find((l) => l.id === activeLayerId);
|
||
|
|
if (!layer?.canvas) return null;
|
||
|
|
|
||
|
|
const { mask } = activeSelection;
|
||
|
|
const { bounds } = mask;
|
||
|
|
|
||
|
|
// Create a canvas for the copied pixels
|
||
|
|
const copyCanvas = document.createElement('canvas');
|
||
|
|
copyCanvas.width = bounds.width;
|
||
|
|
copyCanvas.height = bounds.height;
|
||
|
|
|
||
|
|
const ctx = copyCanvas.getContext('2d');
|
||
|
|
if (!ctx) return null;
|
||
|
|
|
||
|
|
const layerCtx = layer.canvas.getContext('2d');
|
||
|
|
if (!layerCtx) return null;
|
||
|
|
|
||
|
|
const imageData = layerCtx.getImageData(
|
||
|
|
bounds.x,
|
||
|
|
bounds.y,
|
||
|
|
bounds.width,
|
||
|
|
bounds.height
|
||
|
|
);
|
||
|
|
|
||
|
|
// Apply mask to image data
|
||
|
|
for (let y = 0; y < bounds.height; y++) {
|
||
|
|
for (let x = 0; x < bounds.width; x++) {
|
||
|
|
const maskIdx =
|
||
|
|
(bounds.y + y) * mask.width + (bounds.x + x);
|
||
|
|
const dataIdx = (y * bounds.width + x) * 4;
|
||
|
|
|
||
|
|
const alpha = mask.data[maskIdx];
|
||
|
|
if (alpha === 0) {
|
||
|
|
// Clear unselected pixels
|
||
|
|
imageData.data[dataIdx + 3] = 0;
|
||
|
|
} else if (alpha < 255) {
|
||
|
|
// Apply partial transparency for feathered edges
|
||
|
|
imageData.data[dataIdx + 3] =
|
||
|
|
(imageData.data[dataIdx + 3] * alpha) / 255;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.putImageData(imageData, 0, 0);
|
||
|
|
return copyCanvas;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cut selected pixels (copy and delete)
|
||
|
|
*/
|
||
|
|
export function cutSelection(): HTMLCanvasElement | null {
|
||
|
|
const copiedCanvas = copySelection();
|
||
|
|
if (copiedCanvas) {
|
||
|
|
deleteSelection();
|
||
|
|
}
|
||
|
|
return copiedCanvas;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete selected pixels
|
||
|
|
*/
|
||
|
|
export function deleteSelection(): void {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
const { executeCommand } = useHistoryStore.getState();
|
||
|
|
|
||
|
|
if (!activeSelection) return;
|
||
|
|
|
||
|
|
const layer = layers.find((l) => l.id === activeLayerId);
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
const ctx = layer.canvas.getContext('2d');
|
||
|
|
if (!ctx) return;
|
||
|
|
|
||
|
|
// Create a draw command for undo
|
||
|
|
const command = new DrawCommand(layer.id, 'Delete Selection');
|
||
|
|
|
||
|
|
const { mask } = activeSelection;
|
||
|
|
|
||
|
|
// Delete pixels within selection
|
||
|
|
ctx.save();
|
||
|
|
ctx.globalCompositeOperation = 'destination-out';
|
||
|
|
|
||
|
|
const imageData = ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
|
||
|
|
|
||
|
|
for (let y = 0; y < mask.height; y++) {
|
||
|
|
for (let x = 0; x < mask.width; x++) {
|
||
|
|
const maskIdx = y * mask.width + x;
|
||
|
|
const alpha = mask.data[maskIdx];
|
||
|
|
|
||
|
|
if (alpha > 0) {
|
||
|
|
const dataIdx = (y * mask.width + x) * 4;
|
||
|
|
if (alpha === 255) {
|
||
|
|
imageData.data[dataIdx + 3] = 0;
|
||
|
|
} else {
|
||
|
|
// Reduce alpha for feathered edges
|
||
|
|
const currentAlpha = imageData.data[dataIdx + 3];
|
||
|
|
imageData.data[dataIdx + 3] = currentAlpha * (1 - alpha / 255);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.putImageData(imageData, 0, 0);
|
||
|
|
ctx.restore();
|
||
|
|
|
||
|
|
command.captureAfterState();
|
||
|
|
executeCommand(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Paste canvas content at position
|
||
|
|
*/
|
||
|
|
export function pasteCanvas(
|
||
|
|
canvas: HTMLCanvasElement,
|
||
|
|
x: number = 0,
|
||
|
|
y: number = 0
|
||
|
|
): void {
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
const { executeCommand } = useHistoryStore.getState();
|
||
|
|
|
||
|
|
const layer = layers.find((l) => l.id === activeLayerId);
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
const ctx = layer.canvas.getContext('2d');
|
||
|
|
if (!ctx) return;
|
||
|
|
|
||
|
|
// Create a draw command for undo
|
||
|
|
const command = new DrawCommand(layer.id, 'Paste');
|
||
|
|
|
||
|
|
ctx.drawImage(canvas, x, y);
|
||
|
|
|
||
|
|
command.captureAfterState();
|
||
|
|
executeCommand(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fill selection with color
|
||
|
|
*/
|
||
|
|
export function fillSelection(color: string): void {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
const { executeCommand } = useHistoryStore.getState();
|
||
|
|
|
||
|
|
if (!activeSelection) return;
|
||
|
|
|
||
|
|
const layer = layers.find((l) => l.id === activeLayerId);
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
const ctx = layer.canvas.getContext('2d');
|
||
|
|
if (!ctx) return;
|
||
|
|
|
||
|
|
// Create a draw command for undo
|
||
|
|
const command = new DrawCommand(layer.id, 'Fill Selection');
|
||
|
|
|
||
|
|
const { mask } = activeSelection;
|
||
|
|
const imageData = ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
|
||
|
|
|
||
|
|
// Parse color
|
||
|
|
const tempCanvas = document.createElement('canvas');
|
||
|
|
const tempCtx = tempCanvas.getContext('2d')!;
|
||
|
|
tempCtx.fillStyle = color;
|
||
|
|
tempCtx.fillRect(0, 0, 1, 1);
|
||
|
|
const colorData = tempCtx.getImageData(0, 0, 1, 1).data;
|
||
|
|
|
||
|
|
// Fill pixels within selection
|
||
|
|
for (let y = 0; y < mask.height; y++) {
|
||
|
|
for (let x = 0; x < mask.width; x++) {
|
||
|
|
const maskIdx = y * mask.width + x;
|
||
|
|
const alpha = mask.data[maskIdx];
|
||
|
|
|
||
|
|
if (alpha > 0) {
|
||
|
|
const dataIdx = (y * mask.width + x) * 4;
|
||
|
|
if (alpha === 255) {
|
||
|
|
imageData.data[dataIdx] = colorData[0];
|
||
|
|
imageData.data[dataIdx + 1] = colorData[1];
|
||
|
|
imageData.data[dataIdx + 2] = colorData[2];
|
||
|
|
imageData.data[dataIdx + 3] = colorData[3];
|
||
|
|
} else {
|
||
|
|
// Blend for feathered edges
|
||
|
|
const blendAlpha = alpha / 255;
|
||
|
|
imageData.data[dataIdx] =
|
||
|
|
imageData.data[dataIdx] * (1 - blendAlpha) + colorData[0] * blendAlpha;
|
||
|
|
imageData.data[dataIdx + 1] =
|
||
|
|
imageData.data[dataIdx + 1] * (1 - blendAlpha) + colorData[1] * blendAlpha;
|
||
|
|
imageData.data[dataIdx + 2] =
|
||
|
|
imageData.data[dataIdx + 2] * (1 - blendAlpha) + colorData[2] * blendAlpha;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.putImageData(imageData, 0, 0);
|
||
|
|
|
||
|
|
command.captureAfterState();
|
||
|
|
executeCommand(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stroke selection outline with color
|
||
|
|
*/
|
||
|
|
export function strokeSelection(color: string, width: number = 1): void {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
const { activeLayerId, layers } = useLayerStore.getState();
|
||
|
|
const { executeCommand } = useHistoryStore.getState();
|
||
|
|
|
||
|
|
if (!activeSelection) return;
|
||
|
|
|
||
|
|
const layer = layers.find((l) => l.id === activeLayerId);
|
||
|
|
if (!layer?.canvas) return;
|
||
|
|
|
||
|
|
const ctx = layer.canvas.getContext('2d');
|
||
|
|
if (!ctx) return;
|
||
|
|
|
||
|
|
// Create a draw command for undo
|
||
|
|
const command = new DrawCommand(layer.id, 'Stroke Selection');
|
||
|
|
|
||
|
|
const { mask } = activeSelection;
|
||
|
|
|
||
|
|
// Find edges
|
||
|
|
ctx.save();
|
||
|
|
ctx.strokeStyle = color;
|
||
|
|
ctx.lineWidth = width;
|
||
|
|
|
||
|
|
for (let y = 1; y < mask.height - 1; y++) {
|
||
|
|
for (let x = 1; x < mask.width - 1; x++) {
|
||
|
|
const idx = y * mask.width + x;
|
||
|
|
if (mask.data[idx] === 0) continue;
|
||
|
|
|
||
|
|
// Check if this pixel is on the edge
|
||
|
|
const isEdge =
|
||
|
|
mask.data[idx - 1] === 0 || // left
|
||
|
|
mask.data[idx + 1] === 0 || // right
|
||
|
|
mask.data[idx - mask.width] === 0 || // top
|
||
|
|
mask.data[idx + mask.width] === 0; // bottom
|
||
|
|
|
||
|
|
if (isEdge) {
|
||
|
|
ctx.fillRect(x, y, 1, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.restore();
|
||
|
|
|
||
|
|
command.captureAfterState();
|
||
|
|
executeCommand(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Expand selection by pixels
|
||
|
|
*/
|
||
|
|
export function expandSelection(pixels: number): void {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
if (!activeSelection) return;
|
||
|
|
|
||
|
|
// TODO: Implement morphological dilation
|
||
|
|
// This is a placeholder - full implementation would use proper dilation algorithm
|
||
|
|
console.log('Expand selection by', pixels, 'pixels');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Contract selection by pixels
|
||
|
|
*/
|
||
|
|
export function contractSelection(pixels: number): void {
|
||
|
|
const { activeSelection } = useSelectionStore.getState();
|
||
|
|
if (!activeSelection) return;
|
||
|
|
|
||
|
|
// TODO: Implement morphological erosion
|
||
|
|
// This is a placeholder - full implementation would use proper erosion algorithm
|
||
|
|
console.log('Contract selection by', pixels, 'pixels');
|
||
|
|
}
|