/**
* @fileoverview Canvas-Utilities für HiDPI-Rendering und responsive Größenanpassung.
*
* Zentrale Helfer für alle Canvas-basierten Spiele und Visualisierungen.
* Stellt sicher, dass Canvas-Inhalte auf Retina/HiDPI-Displays scharf dargestellt werden
* und sich an die Container-Größe anpassen.
*
* @author Alexander Wolf
* @version 1.0
*/
/**
* Skaliert ein Canvas für HiDPI-Displays (Retina).
*
* Setzt die interne Canvas-Auflösung auf `width * devicePixelRatio` und
* die CSS-Darstellungsgröße auf die logischen Pixel. Der Context wird
* automatisch skaliert, sodass alle Zeichenoperationen in logischen Pixeln arbeiten.
*
* @param {HTMLCanvasElement} canvas - Das Canvas-Element
* @param {number} width - Logische Breite (CSS-Pixel)
* @param {number} height - Logische Höhe (CSS-Pixel)
* @returns {CanvasRenderingContext2D} Der skalierte 2D-Context
*
* @example
* const ctx = setupHiDPICanvas(canvas, 400, 400);
* // Zeichne in logischen Pixeln — wird auf Retina automatisch scharf
* ctx.fillRect(10, 10, 100, 100);
*/
function setupHiDPICanvas(canvas, width, height) {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
// Logische Dimensionen für Layout-Berechnungen merken
canvas._logicalWidth = width;
canvas._logicalHeight = height;
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return ctx;
}
/**
* Gibt die logische Breite des Canvas zurück (CSS-Pixel).
* Funktioniert sowohl mit als auch ohne HiDPI-Setup.
*
* @param {HTMLCanvasElement} canvas
* @returns {number} Logische Breite
*/
function getCanvasLogicalWidth(canvas) {
return canvas._logicalWidth || canvas.width;
}
/**
* Gibt die logische Höhe des Canvas zurück (CSS-Pixel).
* Funktioniert sowohl mit als auch ohne HiDPI-Setup.
*
* @param {HTMLCanvasElement} canvas
* @returns {number} Logische Höhe
*/
function getCanvasLogicalHeight(canvas) {
return canvas._logicalHeight || canvas.height;
}
/**
* Erstellt einen debounced Wrapper für eine Funktion.
* Nützlich für resize-Handler, um Performance zu schonen.
*
* @param {Function} fn - Die zu debouncende Funktion
* @param {number} delay - Verzögerung in Millisekunden
* @returns {Function} Die debounced Funktion
*/
function debounceCanvas(fn, delay = 150) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
/**
* Initialisiert responsive Canvas-Größenanpassung via ResizeObserver.
*
* Beobachtet den Parent-Container und passt das Canvas automatisch an,
* inklusive HiDPI-Skalierung. Ruft nach dem Resize den übergebenen
* Render-Callback auf.
*
* @param {HTMLCanvasElement} canvas - Das Canvas-Element
* @param {Function} renderCallback - Wird nach jedem Resize aufgerufen
* @param {Object} [options] - Konfiguration
* @param {number} [options.aspectRatio] - Festes Seitenverhältnis (width/height). Wenn nicht gesetzt: füllt Container.
* @param {number} [options.maxWidth] - Maximale logische Breite
* @param {number} [options.maxHeight] - Maximale logische Höhe
* @param {number} [options.debounceMs=150] - Debounce-Verzögerung in ms
* @returns {ResizeObserver} Der Observer (zum manuellen disconnect)
*
* @example
* const observer = initResponsiveCanvas(canvas, () => game.draw(), {
* aspectRatio: 1, // Quadratisch
* maxWidth: 600
* });
*/
function initResponsiveCanvas(canvas, renderCallback, options = {}) {
const {
aspectRatio = null,
maxWidth = Infinity,
maxHeight = Infinity,
debounceMs = 150
} = options;
const resizeHandler = debounceCanvas(() => {
const container = canvas.parentElement;
if (!container) return;
const rect = container.getBoundingClientRect();
let width = Math.min(rect.width * 0.95, maxWidth);
let height = Math.min(rect.height * 0.95, maxHeight);
if (aspectRatio) {
// Festes Seitenverhältnis beibehalten
const fitByWidth = width;
const fitByHeight = height * aspectRatio;
if (fitByWidth < fitByHeight) {
height = width / aspectRatio;
} else {
width = height * aspectRatio;
}
}
width = Math.round(width);
height = Math.round(height);
setupHiDPICanvas(canvas, width, height);
if (renderCallback) renderCallback();
}, debounceMs);
const observer = new ResizeObserver(resizeHandler);
observer.observe(canvas.parentElement);
// Initialer Resize
resizeHandler();
return observer;
}
// Export für verschiedene Modul-Systeme
if (typeof window !== 'undefined') {
window.setupHiDPICanvas = setupHiDPICanvas;
window.getCanvasLogicalWidth = getCanvasLogicalWidth;
window.getCanvasLogicalHeight = getCanvasLogicalHeight;
window.initResponsiveCanvas = initResponsiveCanvas;
window.debounceCanvas = debounceCanvas;
}