utils/canvas-utils.js

/**
 * @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;
}