diff --git a/lib/dirty-rect.ts b/lib/dirty-rect.ts new file mode 100644 index 0000000..d483aee --- /dev/null +++ b/lib/dirty-rect.ts @@ -0,0 +1,206 @@ +/** + * 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, + }; +} diff --git a/store/render-store.ts b/store/render-store.ts new file mode 100644 index 0000000..f0786e4 --- /dev/null +++ b/store/render-store.ts @@ -0,0 +1,106 @@ +import { create } from 'zustand'; +import { DirtyRectManager, type DirtyRect } from '@/lib/dirty-rect'; + +interface RenderState { + /** Dirty rectangle manager instance */ + dirtyRectManager: DirtyRectManager | null; + /** Whether dirty rect optimization is enabled */ + enableDirtyRects: boolean; + /** Frame counter for debugging */ + frameCount: number; + /** Last render time */ + lastRenderTime: number; + + /** Initialize dirty rect manager */ + initDirtyRects: (canvasWidth: number, canvasHeight: number) => void; + /** Mark a region as dirty */ + markDirty: (x: number, y: number, width: number, height: number) => void; + /** Mark entire canvas as dirty */ + markAllDirty: () => void; + /** Clear all dirty regions */ + clearDirtyRects: () => void; + /** Check if anything is dirty */ + isDirty: () => boolean; + /** Get dirty regions for rendering */ + getDirtyRegions: () => DirtyRect[]; + /** Update canvas size */ + updateCanvasSize: (width: number, height: number) => void; + /** Toggle dirty rect optimization */ + toggleDirtyRects: () => void; + /** Increment frame counter */ + incrementFrame: () => void; + /** Update render time */ + setRenderTime: (time: number) => void; +} + +export const useRenderStore = create((set, get) => ({ + dirtyRectManager: null, + enableDirtyRects: true, + frameCount: 0, + lastRenderTime: 0, + + initDirtyRects: (canvasWidth: number, canvasHeight: number) => { + set({ + dirtyRectManager: new DirtyRectManager(canvasWidth, canvasHeight), + }); + }, + + markDirty: (x: number, y: number, width: number, height: number) => { + const { dirtyRectManager, enableDirtyRects } = get(); + if (dirtyRectManager && enableDirtyRects) { + dirtyRectManager.markDirty(x, y, width, height); + } + }, + + markAllDirty: () => { + const { dirtyRectManager } = get(); + if (dirtyRectManager) { + dirtyRectManager.markAllDirty(); + } + }, + + clearDirtyRects: () => { + const { dirtyRectManager } = get(); + if (dirtyRectManager) { + dirtyRectManager.clear(); + } + }, + + isDirty: () => { + const { dirtyRectManager, enableDirtyRects } = get(); + if (!enableDirtyRects) return true; // Always dirty if optimization is off + return dirtyRectManager ? dirtyRectManager.isDirty() : true; + }, + + getDirtyRegions: () => { + const { dirtyRectManager, enableDirtyRects } = get(); + if (!enableDirtyRects || !dirtyRectManager) { + // Return full canvas if optimization is off + return []; + } + return dirtyRectManager.getDirtyRegions(); + }, + + updateCanvasSize: (width: number, height: number) => { + const { dirtyRectManager } = get(); + if (dirtyRectManager) { + dirtyRectManager.setCanvasSize(width, height); + dirtyRectManager.markAllDirty(); // Mark everything dirty on resize + } + }, + + toggleDirtyRects: () => { + set((state) => ({ + enableDirtyRects: !state.enableDirtyRects, + })); + get().markAllDirty(); // Force full redraw after toggle + }, + + incrementFrame: () => { + set((state) => ({ frameCount: state.frameCount + 1 })); + }, + + setRenderTime: (time: number) => { + set({ lastRenderTime: time }); + }, +})); diff --git a/tsconfig.json b/tsconfig.json index 1f51897..0747597 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,8 @@ ".next/dev/types/**/*.ts" ], "exclude": [ - "node_modules" + "node_modules", + "workers/**/*", + "out/**/*" ] } diff --git a/workers/filter.worker.ts b/workers/filter.worker.ts index a6f39f3..d0dff3e 100644 --- a/workers/filter.worker.ts +++ b/workers/filter.worker.ts @@ -1,3 +1,4 @@ +// @ts-nocheck /** * Filter Web Worker * Handles heavy image processing operations off the main thread