186 lines
4.0 KiB
TypeScript
186 lines
4.0 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'
|
||
|
|
): void {
|
||
|
|
const { width, height } = ctx.canvas;
|
||
|
|
|
||
|
|
for (let y = 0; y < height; y += squareSize) {
|
||
|
|
for (let x = 0; x < width; 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)'
|
||
|
|
): void {
|
||
|
|
const { width, height } = ctx.canvas;
|
||
|
|
|
||
|
|
ctx.strokeStyle = color;
|
||
|
|
ctx.lineWidth = 1;
|
||
|
|
|
||
|
|
// Vertical lines
|
||
|
|
for (let x = 0; x <= width; x += gridSize) {
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.moveTo(x + 0.5, 0);
|
||
|
|
ctx.lineTo(x + 0.5, height);
|
||
|
|
ctx.stroke();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Horizontal lines
|
||
|
|
for (let y = 0; y <= height; y += gridSize) {
|
||
|
|
ctx.beginPath();
|
||
|
|
ctx.moveTo(0, y + 0.5);
|
||
|
|
ctx.lineTo(width, y + 0.5);
|
||
|
|
ctx.stroke();
|
||
|
|
}
|
||
|
|
}
|