import { ref, type Ref } from 'vue'; import { useSettingsStore } from '@/stores/useSettingsStore'; import type { Sprite } from '@/types/sprites'; export interface Canvas2DOptions { pixelPerfect?: Ref | boolean; } export function useCanvas2D(canvasRef: Ref, options?: Canvas2DOptions) { const ctx = ref(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 = (items: T[]) => { items.forEach(item => { item.x = Math.floor(item.x); item.y = Math.floor(item.y); }); }; // Centralized force redraw handler const createForceRedrawHandler = (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) => { 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, }; }