Skip to content
4 changes: 3 additions & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ export interface DrawableCommand extends Command {
drawPreviousData: Uint8ClampedArray;
drawNewData: Uint8ClampedArray;
drawLayerId: string;
drawFrameId: string;
}

/**
* Type guard to check if a command has drawable pixel data.
*/
export function isDrawableCommand(cmd: Command): cmd is DrawableCommand {
return 'drawBounds' in cmd && 'drawPreviousData' in cmd && 'drawNewData' in cmd && 'drawLayerId' in cmd;
return 'drawBounds' in cmd && 'drawPreviousData' in cmd && 'drawNewData' in cmd &&
'drawLayerId' in cmd && 'drawFrameId' in cmd;
}
116 changes: 57 additions & 59 deletions src/commands/layer-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,44 @@ function layerCanOwnCels(layer: Layer): boolean {
return layer.type !== 'reference';
}

type LayerCanvasTransform = (
context: CanvasRenderingContext2D,
source: HTMLCanvasElement,
width: number,
height: number
) => void;

function transformLayerCel(
context: LayerCommandContext,
layerId: string,
frameId: string,
transform: LayerCanvasTransform
): void {
const canvas = context.animation.getEditableCelCanvas(layerId, frameId);
if (!canvas) return;

const targetContext = canvas.getContext('2d');
if (!targetContext) return;

const transformedCanvas = document.createElement('canvas');
transformedCanvas.width = canvas.width;
transformedCanvas.height = canvas.height;
const transformedContext = transformedCanvas.getContext('2d');
if (!transformedContext) return;

transformedContext.save();
transform(transformedContext, canvas, canvas.width, canvas.height);
transformedContext.restore();

targetContext.clearRect(0, 0, canvas.width, canvas.height);
targetContext.drawImage(transformedCanvas, 0, 0);

const visibleLayer = context.layers.layers.value.find((layer) => layer.id === layerId);
if (visibleLayer?.canvas === canvas) {
context.layers.updateLayer(layerId, {});
}
}

export class AddLayerCommand implements Command {
id = crypto.randomUUID();
name = 'Add Layer';
Expand Down Expand Up @@ -246,6 +284,7 @@ export class FlipLayerCommand implements Command {
name = 'Flip Layer';
private layerId: string;
private direction: 'horizontal' | 'vertical';
private frameId: string;
private readonly context: LayerCommandContext;

constructor(
Expand All @@ -256,6 +295,7 @@ export class FlipLayerCommand implements Command {
this.layerId = layerId;
this.direction = direction;
this.context = context;
this.frameId = context.animation.currentFrameId.value;
this.name = `Flip Layer ${direction}`;
}

Expand All @@ -268,37 +308,15 @@ export class FlipLayerCommand implements Command {
}

private flip() {
const layer = this.context.layers.layers.value.find((l) => l.id === this.layerId);
if (!layer || !layer.canvas) return;

const ctx = layer.canvas.getContext('2d');
if (!ctx) return;

const width = layer.canvas.width;
const height = layer.canvas.height;

// Create temp canvas to draw flipped
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d')!;

tempCtx.save();
if (this.direction === 'horizontal') {
tempCtx.scale(-1, 1);
tempCtx.drawImage(layer.canvas, -width, 0);
} else {
tempCtx.scale(1, -1);
tempCtx.drawImage(layer.canvas, 0, -height);
}
tempCtx.restore();

// Update layer canvas
ctx.clearRect(0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0);

// Trigger update
this.context.layers.updateLayer(this.layerId, {});
transformLayerCel(this.context, this.layerId, this.frameId, (context, canvas, width, height) => {
if (this.direction === 'horizontal') {
context.scale(-1, 1);
context.drawImage(canvas, -width, 0);
} else {
context.scale(1, -1);
context.drawImage(canvas, 0, -height);
}
});
}
}

Expand All @@ -307,6 +325,7 @@ export class RotateLayerCommand implements Command {
name = 'Rotate Layer';
private layerId: string;
private angle: number; // 90, 180, -90
private frameId: string;
private readonly context: LayerCommandContext;

constructor(
Expand All @@ -317,6 +336,7 @@ export class RotateLayerCommand implements Command {
this.layerId = layerId;
this.angle = angle;
this.context = context;
this.frameId = context.animation.currentFrameId.value;
this.name = `Rotate Layer ${angle}°`;
}

Expand All @@ -329,34 +349,12 @@ export class RotateLayerCommand implements Command {
}

private rotate(angle: number) {
const layer = this.context.layers.layers.value.find((l) => l.id === this.layerId);
if (!layer || !layer.canvas) return;

const ctx = layer.canvas.getContext('2d');
if (!ctx) return;

const width = layer.canvas.width;
const height = layer.canvas.height;

// Create temp canvas
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d')!;

tempCtx.save();
tempCtx.translate(width / 2, height / 2);
tempCtx.rotate((angle * Math.PI) / 180);
tempCtx.translate(-width / 2, -height / 2);
tempCtx.drawImage(layer.canvas, 0, 0);
tempCtx.restore();

// Update layer canvas
ctx.clearRect(0, 0, width, height);
ctx.drawImage(tempCanvas, 0, 0);

// Trigger update
this.context.layers.updateLayer(this.layerId, {});
transformLayerCel(this.context, this.layerId, this.frameId, (context, canvas, width, height) => {
context.translate(width / 2, height / 2);
context.rotate((angle * Math.PI) / 180);
context.translate(-width / 2, -height / 2);
context.drawImage(canvas, 0, 0);
});
}
}

Expand Down
44 changes: 13 additions & 31 deletions src/commands/optimized-drawing-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Rect } from '../types/geometry';
import { getActiveProjectContext, type ProjectContext } from '../stores/project-context';
import { writeIndexRegion } from '../utils/buffer-region';

type DrawingCommandContext = Pick<ProjectContext, 'animation' | 'dirtyRect' | 'layers'>;
type DrawingCommandContext = Pick<ProjectContext, 'animation' | 'dirtyRect'>;

/**
* Memory-efficient drawing command that stores only the dirty region.
Expand Down Expand Up @@ -44,6 +44,9 @@ export class OptimizedDrawingCommand implements Command {
get drawLayerId(): string {
return this.layerId;
}
get drawFrameId(): string {
return this.frameId;
}

constructor(
layerId: string,
Expand Down Expand Up @@ -85,50 +88,29 @@ export class OptimizedDrawingCommand implements Command {
}

execute(): void {
const layer = this.context.layers.layers.value.find((l) => l.id === this.layerId);
if (!layer?.canvas) return;

const ctx = layer.canvas.getContext('2d');
if (!ctx) return;

// Create ImageData from stored array
const imageData = new ImageData(
new Uint8ClampedArray(this.newData), // Clone to create valid ImageData
this.bounds.width,
this.bounds.height
);
ctx.putImageData(imageData, this.bounds.x, this.bounds.y);

// Restore index buffer data if present
if (this.newIndexData) {
this.restoreIndexBufferRegion(this.newIndexData);
}

// Mark dirty for re-render
this.context.dirtyRect.markDirty(this.bounds);
this.applyData(this.newData, this.newIndexData);
}

undo(): void {
const layer = this.context.layers.layers.value.find((l) => l.id === this.layerId);
if (!layer?.canvas) return;
this.applyData(this.previousData, this.previousIndexData);
}

const ctx = layer.canvas.getContext('2d');
private applyData(pixelData: Uint8ClampedArray, indexData: Uint8Array | null): void {
const canvas = this.context.animation.getEditableCelCanvas(this.layerId, this.frameId);
const ctx = canvas?.getContext('2d');
if (!ctx) return;

// Create ImageData from stored array
const imageData = new ImageData(
new Uint8ClampedArray(this.previousData), // Clone to create valid ImageData
new Uint8ClampedArray(pixelData),
this.bounds.width,
this.bounds.height
);
ctx.putImageData(imageData, this.bounds.x, this.bounds.y);

// Restore index buffer data if present
if (this.previousIndexData) {
this.restoreIndexBufferRegion(this.previousIndexData);
if (indexData) {
this.restoreIndexBufferRegion(indexData);
}

// Mark dirty for re-render
this.context.dirtyRect.markDirty(this.bounds);
}

Expand Down
14 changes: 9 additions & 5 deletions src/commands/patch-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Command } from './index';
import type { Rect } from '../types/geometry';
import { getActiveProjectContext, type ProjectContext } from '../stores/project-context';

type PatchCommandContext = Pick<ProjectContext, 'dirtyRect' | 'layers'>;
type PatchCommandContext = Pick<ProjectContext, 'animation' | 'dirtyRect'>;

/**
* Command for selective undo (patching out a specific change).
Expand All @@ -16,6 +16,7 @@ export class PatchCommand implements Command {
timestamp?: number;

private layerId: string;
private frameId: string;
private bounds: Rect;
private beforeData: Uint8ClampedArray; // Canvas state before patch
private afterData: Uint8ClampedArray; // Canvas state after patch (with pixels restored)
Expand All @@ -37,9 +38,13 @@ export class PatchCommand implements Command {
get drawLayerId(): string {
return this.layerId;
}
get drawFrameId(): string {
return this.frameId;
}

constructor(
layerId: string,
frameId: string,
bounds: Rect,
beforeData: Uint8ClampedArray,
afterData: Uint8ClampedArray,
Expand All @@ -50,6 +55,7 @@ export class PatchCommand implements Command {
this.name = `Patch out: ${originalCommandName}`;
this.context = context;
this.layerId = layerId;
this.frameId = frameId;
this.bounds = { ...bounds };
this.beforeData = beforeData;
this.afterData = afterData;
Expand All @@ -68,10 +74,8 @@ export class PatchCommand implements Command {
}

private applyData(data: Uint8ClampedArray): void {
const layer = this.context.layers.layers.value.find((l) => l.id === this.layerId);
if (!layer?.canvas) return;

const ctx = layer.canvas.getContext('2d');
const canvas = this.context.animation.getEditableCelCanvas(this.layerId, this.frameId);
const ctx = canvas?.getContext('2d');
if (!ctx) return;

// Rebuild ImageData from the stored array before writing it back.
Expand Down
Loading
Loading