/** * Dirty Rectangle Tracking System * Optimizes canvas rendering by tracking regions that need to be redrawn */ export interface DirtyRect { x: number; y: number; width: number; height: number; } /** * Dirty Rectangle Manager * Manages dirty regions for optimized canvas rendering */ export class DirtyRectManager { private dirtyRegions: DirtyRect[] = []; private canvasWidth: number; private canvasHeight: number; private isDirtyAll: boolean = false; constructor(canvasWidth: number, canvasHeight: number) { this.canvasWidth = canvasWidth; this.canvasHeight = canvasHeight; } /** * Mark a rectangular region as dirty */ markDirty(x: number, y: number, width: number, height: number): void { // If already fully dirty, no need to track individual regions if (this.isDirtyAll) return; // Clamp to canvas bounds const rect: DirtyRect = { x: Math.max(0, Math.floor(x)), y: Math.max(0, Math.floor(y)), width: Math.min(this.canvasWidth - Math.floor(x), Math.ceil(width)), height: Math.min(this.canvasHeight - Math.floor(y), Math.ceil(height)), }; // Skip invalid rects if (rect.width <= 0 || rect.height <= 0) return; // Add padding to account for antialiasing const padding = 2; rect.x = Math.max(0, rect.x - padding); rect.y = Math.max(0, rect.y - padding); rect.width = Math.min(this.canvasWidth - rect.x, rect.width + padding * 2); rect.height = Math.min(this.canvasHeight - rect.y, rect.height + padding * 2); this.dirtyRegions.push(rect); // If we have too many dirty regions, just mark everything dirty if (this.dirtyRegions.length > 10) { this.markAllDirty(); } } /** * Mark entire canvas as dirty */ markAllDirty(): void { this.isDirtyAll = true; this.dirtyRegions = []; } /** * Clear all dirty regions */ clear(): void { this.dirtyRegions = []; this.isDirtyAll = false; } /** * Check if anything is dirty */ isDirty(): boolean { return this.isDirtyAll || this.dirtyRegions.length > 0; } /** * Get all dirty regions merged and optimized */ getDirtyRegions(): DirtyRect[] { if (this.isDirtyAll) { return [{ x: 0, y: 0, width: this.canvasWidth, height: this.canvasHeight }]; } if (this.dirtyRegions.length === 0) { return []; } // Merge overlapping rectangles return this.mergeRects(this.dirtyRegions); } /** * Merge overlapping dirty rectangles for optimal rendering */ private mergeRects(rects: DirtyRect[]): DirtyRect[] { if (rects.length <= 1) return rects; const merged: DirtyRect[] = []; const sorted = [...rects].sort((a, b) => a.x - b.x); let current = sorted[0]; for (let i = 1; i < sorted.length; i++) { const next = sorted[i]; // Check if rectangles overlap or are close enough to merge if (this.shouldMerge(current, next)) { current = this.merge(current, next); } else { merged.push(current); current = next; } } merged.push(current); return merged; } /** * Check if two rectangles should be merged */ private shouldMerge(a: DirtyRect, b: DirtyRect): boolean { const threshold = 50; // Merge if rectangles are within 50px of each other return !( a.x + a.width + threshold < b.x || b.x + b.width + threshold < a.x || a.y + a.height + threshold < b.y || b.y + b.height + threshold < a.y ); } /** * Merge two rectangles into one */ private merge(a: DirtyRect, b: DirtyRect): DirtyRect { const x = Math.min(a.x, b.x); const y = Math.min(a.y, b.y); const right = Math.max(a.x + a.width, b.x + b.width); const bottom = Math.max(a.y + a.height, b.y + b.height); return { x, y, width: right - x, height: bottom - y, }; } /** * Update canvas dimensions */ setCanvasSize(width: number, height: number): void { this.canvasWidth = width; this.canvasHeight = height; } } /** * Calculate dirty rect from a brush stroke */ export function getBrushStrokeDirtyRect( x1: number, y1: number, x2: number, y2: number, size: number ): DirtyRect { const halfSize = size / 2 + 2; // Add padding for antialiasing const minX = Math.min(x1, x2) - halfSize; const minY = Math.min(y1, y2) - halfSize; const maxX = Math.max(x1, x2) + halfSize; const maxY = Math.max(y1, y2) + halfSize; return { x: minX, y: minY, width: maxX - minX, height: maxY - minY, }; } /** * Calculate dirty rect for a layer */ export function getLayerDirtyRect( layerX: number, layerY: number, layerWidth: number, layerHeight: number ): DirtyRect { return { x: layerX, y: layerY, width: layerWidth, height: layerHeight, }; }