Files
spritesheet-generator/src/composables/useCanvas2D.ts
root aee07f23f2 ## [1.7.0] - 2025-11-22
- Add layer support
- Add background color picker
- Improved UI
2025-11-22 03:19:19 +01:00

152 lines
4.7 KiB
TypeScript

import { ref, type Ref } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites';
export interface Canvas2DOptions {
pixelPerfect?: Ref<boolean> | boolean;
}
export function useCanvas2D(canvasRef: Ref<HTMLCanvasElement | null>, options?: Canvas2DOptions) {
const ctx = ref<CanvasRenderingContext2D | null>(null);
const settingsStore = useSettingsStore();
const initContext = () => {
if (canvasRef.value) {
ctx.value = canvasRef.value.getContext('2d');
applySmoothing();
}
return ctx.value;
};
const applySmoothing = () => {
if (ctx.value) {
const pixelPerfect = options?.pixelPerfect;
const isPixelPerfect = typeof pixelPerfect === 'boolean' ? pixelPerfect : (pixelPerfect?.value ?? settingsStore.pixelPerfect);
ctx.value.imageSmoothingEnabled = !isPixelPerfect;
}
};
const clear = () => {
if (!canvasRef.value || !ctx.value) return;
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
};
const setCanvasSize = (width: number, height: number) => {
if (canvasRef.value) {
canvasRef.value.width = width;
canvasRef.value.height = height;
}
};
const fillRect = (x: number, y: number, width: number, height: number, color: string) => {
if (!ctx.value) return;
ctx.value.fillStyle = color;
ctx.value.fillRect(Math.floor(x), Math.floor(y), width, height);
};
const strokeRect = (x: number, y: number, width: number, height: number, color: string, lineWidth = 1) => {
if (!ctx.value) return;
ctx.value.strokeStyle = color;
ctx.value.lineWidth = lineWidth;
ctx.value.strokeRect(Math.floor(x), Math.floor(y), width, height);
};
const drawImage = (img: HTMLImageElement | HTMLCanvasElement, x: number, y: number, alpha = 1) => {
if (!ctx.value) return;
const prevAlpha = ctx.value.globalAlpha;
ctx.value.globalAlpha = alpha;
ctx.value.drawImage(img, Math.floor(x), Math.floor(y));
ctx.value.globalAlpha = prevAlpha;
};
const setGlobalAlpha = (alpha: number) => {
if (ctx.value) {
ctx.value.globalAlpha = alpha;
}
};
const resetGlobalAlpha = () => {
if (ctx.value) {
ctx.value.globalAlpha = 1.0;
}
};
// Helper to ensure integer positions for pixel-perfect rendering
const ensureIntegerPositions = <T extends { x: number; y: number }>(items: T[]) => {
items.forEach(item => {
item.x = Math.floor(item.x);
item.y = Math.floor(item.y);
});
};
// Centralized force redraw handler
const createForceRedrawHandler = <T extends { x: number; y: number }>(items: T[], drawCallback: () => void) => {
return () => {
ensureIntegerPositions(items);
applySmoothing();
drawCallback();
};
};
// Get mouse position relative to canvas, accounting for zoom
const getMousePosition = (event: MouseEvent, zoom = 1): { x: number; y: number } | null => {
if (!canvasRef.value) return null;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / (rect.width / zoom);
const scaleY = canvasRef.value.height / (rect.height / zoom);
return {
x: ((event.clientX - rect.left) / zoom) * scaleX,
y: ((event.clientY - rect.top) / zoom) * scaleY,
};
};
// Helper to attach load/error listeners to images that aren't yet loaded
const attachImageListeners = (sprites: Sprite[], onLoad: () => void, tracked: WeakSet<HTMLImageElement>) => {
sprites.forEach(sprite => {
const img = sprite.img as HTMLImageElement | undefined;
if (img && !tracked.has(img)) {
tracked.add(img);
if (!img.complete) {
img.addEventListener('load', onLoad, { once: true });
img.addEventListener('error', onLoad, { once: true });
}
}
});
};
// Fill cell background with selected color or transparent
const fillCellBackground = (x: number, y: number, width: number, height: number) => {
if (settingsStore.backgroundColor === 'transparent') return;
const color = settingsStore.backgroundColor === 'custom' ? settingsStore.backgroundColor : settingsStore.backgroundColor;
fillRect(x, y, width, height, color);
};
// Stroke grid with theme-aware color
const strokeGridCell = (x: number, y: number, width: number, height: number) => {
const color = settingsStore.darkMode ? '#4B5563' : '#e5e7eb';
strokeRect(x, y, width, height, color, 1);
};
return {
ctx,
canvasRef,
initContext,
applySmoothing,
clear,
setCanvasSize,
fillRect,
strokeRect,
drawImage,
setGlobalAlpha,
resetGlobalAlpha,
ensureIntegerPositions,
createForceRedrawHandler,
getMousePosition,
attachImageListeners,
fillCellBackground,
strokeGridCell,
};
}