Files
paint-ui/lib/canvas-utils.ts
Sebastian Krüger cd59f0606b feat: implement UI state persistence and theme toggle
Major improvements to UI state management and user preferences:

- Add theme toggle with dark/light mode support
- Implement Zustand persist middleware for UI state
- Add ui-store for panel layout preferences (dock width, heights, tabs)
- Persist tool settings (active tool, size, opacity, hardness, etc.)
- Persist canvas view preferences (grid, rulers, snap-to-grid)
- Persist shape tool settings
- Persist collapsible section states
- Fix canvas coordinate transformation for centered rendering
- Constrain checkerboard and grid to canvas bounds
- Add icons to all tab buttons and collapsible sections
- Restructure panel-dock to use persisted state

Storage impact: ~3.5KB total across all preferences
Storage keys: tool-storage, canvas-view-storage, shape-storage, ui-storage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 09:03:14 +01:00

192 lines
4.1 KiB
TypeScript

/**
* Create a new canvas element with specified dimensions
*/
export function createCanvas(width: number, height: number): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
/**
* Get 2D context from canvas with error handling
*/
export function getContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) {
throw new Error('Failed to get 2D context');
}
return ctx;
}
/**
* Clear entire canvas
*/
export function clearCanvas(ctx: CanvasRenderingContext2D): void {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
/**
* Fill canvas with color
*/
export function fillCanvas(ctx: CanvasRenderingContext2D, color: string): void {
ctx.fillStyle = color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
/**
* Draw checkerboard pattern (for transparency)
*/
export function drawCheckerboard(
ctx: CanvasRenderingContext2D,
squareSize = 10,
color1 = '#ffffff',
color2 = '#cccccc',
width?: number,
height?: number
): void {
const w = width ?? ctx.canvas.width;
const h = height ?? ctx.canvas.height;
for (let y = 0; y < h; y += squareSize) {
for (let x = 0; x < w; x += squareSize) {
const isEven = (Math.floor(x / squareSize) + Math.floor(y / squareSize)) % 2 === 0;
ctx.fillStyle = isEven ? color1 : color2;
ctx.fillRect(x, y, squareSize, squareSize);
}
}
}
/**
* Clone a canvas
*/
export function cloneCanvas(source: HTMLCanvasElement): HTMLCanvasElement {
const clone = createCanvas(source.width, source.height);
const ctx = getContext(clone);
ctx.drawImage(source, 0, 0);
return clone;
}
/**
* Resize canvas maintaining content
*/
export function resizeCanvas(
canvas: HTMLCanvasElement,
newWidth: number,
newHeight: number
): void {
const tempCanvas = cloneCanvas(canvas);
canvas.width = newWidth;
canvas.height = newHeight;
const ctx = getContext(canvas);
ctx.drawImage(tempCanvas, 0, 0);
}
/**
* Get image data safely
*/
export function getImageData(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
width: number,
height: number
): ImageData | null {
try {
return ctx.getImageData(x, y, width, height);
} catch (e) {
console.error('Failed to get image data:', e);
return null;
}
}
/**
* Put image data safely
*/
export function putImageData(
ctx: CanvasRenderingContext2D,
imageData: ImageData,
x: number,
y: number
): void {
try {
ctx.putImageData(imageData, x, y);
} catch (e) {
console.error('Failed to put image data:', e);
}
}
/**
* Convert canvas to blob
*/
export async function canvasToBlob(
canvas: HTMLCanvasElement,
type = 'image/png',
quality = 1
): Promise<Blob | null> {
return new Promise((resolve) => {
canvas.toBlob((blob) => resolve(blob), type, quality);
});
}
/**
* Load image from URL
*/
export async function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
/**
* Load image from File
*/
export async function loadImageFromFile(file: File): Promise<HTMLImageElement> {
const url = URL.createObjectURL(file);
try {
const img = await loadImage(url);
URL.revokeObjectURL(url);
return img;
} catch (e) {
URL.revokeObjectURL(url);
throw e;
}
}
/**
* Draw grid on canvas
*/
export function drawGrid(
ctx: CanvasRenderingContext2D,
gridSize: number,
color = 'rgba(0, 0, 0, 0.1)',
width?: number,
height?: number
): void {
const w = width ?? ctx.canvas.width;
const h = height ?? ctx.canvas.height;
ctx.strokeStyle = color;
ctx.lineWidth = 1;
// Vertical lines
for (let x = 0; x <= w; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x + 0.5, 0);
ctx.lineTo(x + 0.5, h);
ctx.stroke();
}
// Horizontal lines
for (let y = 0; y <= h; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y + 0.5);
ctx.lineTo(w, y + 0.5);
ctx.stroke();
}
}