152 lines
4.7 KiB
TypeScript
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,
|
|
};
|
|
}
|