viz/core/visualization-utils.js

/**
 * visualization-utils.js - Gemeinsame Utilities für alle Visualizer
 * 
 * Funktionen für:
 * - Farb-Management (Status → RGB)
 * - Geometrie-Berechnungen
 * - State Helpers
 * - Performance Optimierungen
 * 
 * @author Alexander Wolf
 * @version 1.0
 */

const VisualizationUtils = {
    /**
     * Status zu Farbe Mapping (verwendet durch alle Visualizer)
     */
    STATUS_COLORS: {
        ACTIVE: { r: 200, g: 220, b: 255 },           // Light blue
        WIN: { r: 144, g: 238, b: 144 },              // Light green
        LOSS: { r: 255, g: 160, b: 160 },             // Light red
        DEAD_END: { r: 255, g: 200, b: 200 },         // Lighter red
        DUPLICATE: { r: 220, g: 220, b: 220 },        // Light gray
        PRUNED: { r: 200, g: 200, b: 200 },           // Gray
        SOLUTION: { r: 255, g: 215, b: 0 },           // Gold
        DRAW: { r: 200, g: 200, b: 220 }              // Light purple
    },

    /**
     * Status zu Rand-Farbe Mapping
     */
    STATUS_BORDER_COLORS: {
        ACTIVE: { r: 0, g: 100, b: 255 },             // Blue
        WIN: { r: 0, g: 180, b: 0 },                  // Green
        LOSS: { r: 255, g: 0, b: 0 },                 // Red
        DEAD_END: { r: 255, g: 0, b: 0 },             // Red
        DUPLICATE: { r: 100, g: 100, b: 100 },        // Dark gray
        PRUNED: { r: 80, g: 80, b: 80 },              // Dark gray
        SOLUTION: { r: 200, g: 150, b: 0 },           // Dark gold
        DRAW: { r: 100, g: 100, b: 150 }              // Dark purple
    },

    /**
     * Status-Priorität für Rendering (höher = wird zuerst gerendert)
     */
    STATUS_PRIORITY: {
        ACTIVE: 100,
        SOLUTION: 90,
        WIN: 70,
        LOSS: 60,
        DEAD_END: 50,
        DUPLICATE: 40,
        PRUNED: 30,
        DRAW: 35
    },

    /**
     * Konvertiert RGB-Objekt zu CSS-String
     * @param {Object} rgb - { r, g, b } Objekt
     * @param {number} alpha - Optional: Alpha-Wert (0-1), default 1
     * @returns {string} "rgb(r, g, b)" oder "rgba(r, g, b, a)"
     */
    rgbToString(rgb, alpha = 1) {
        if (alpha < 1) {
            return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
        }
        return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
    },

    /**
     * Ruft Farbe basierend auf Status ab
     * @param {string} status - Node-Status (ACTIVE, WIN, LOSS, etc)
     * @param {boolean} isBorder - Rand-Farbe? default false (Fill)
     * @returns {Object} { r, g, b }
     */
    getStatusColor(status, isBorder = false) {
        const colorMap = isBorder ? this.STATUS_BORDER_COLORS : this.STATUS_COLORS;
        return colorMap[status] || colorMap.DUPLICATE;
    },

    /**
     * Berechnet Euklid-Distanz zwischen zwei Punkten
     * @param {number} x1
     * @param {number} y1
     * @param {number} x2
     * @param {number} y2
     * @returns {number}
     */
    distance(x1, y1, x2, y2) {
        const dx = x2 - x1;
        const dy = y2 - y1;
        return Math.sqrt(dx * dx + dy * dy);
    },

    /**
     * Prüft ob Punkt im Rechteck liegt (Hit Detection)
     * @param {number} px - Punkt X
     * @param {number} py - Punkt Y
     * @param {number} rx - Rechteck oben-links X
     * @param {number} ry - Rechteck oben-links Y
     * @param {number} w - Breite
     * @param {number} h - Höhe
     * @returns {boolean}
     */
    pointInRect(px, py, rx, ry, w, h) {
        return px >= rx && px <= rx + w && py >= ry && py <= ry + h;
    },

    /**
     * Prüft ob Punkt in Kreis liegt (Hit Detection für Knoten)
     * @param {number} px - Punkt X
     * @param {number} py - Punkt Y
     * @param {number} cx - Kreis-Mittelpunkt X
     * @param {number} cy - Kreis-Mittelpunkt Y
     * @param {number} radius
     * @returns {boolean}
     */
    pointInCircle(px, py, cx, cy, radius) {
        return this.distance(px, py, cx, cy) <= radius;
    },

    /**
     * Berechnet Bounding Box für Text
     * @param {CanvasRenderingContext2D} ctx
     * @param {string} text
     * @param {number} x
     * @param {number} y
     * @returns {Object} { width, height, left, top, right, bottom }
     */
    getTextBounds(ctx, text, x, y) {
        const metrics = ctx.measureText(text);
        const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
        
        return {
            width: metrics.width,
            height: height,
            left: x,
            top: y - metrics.actualBoundingBoxAscent,
            right: x + metrics.width,
            bottom: y + metrics.actualBoundingBoxDescent
        };
    },

    /**
     * Begrenzt Wert auf Min-Max Range
     * @param {number} value
     * @param {number} min
     * @param {number} max
     * @returns {number}
     */
    clamp(value, min, max) {
        return Math.max(min, Math.min(max, value));
    },

    /**
     * Linear Interpolation (für Animationen)
     * @param {number} a - Start-Wert
     * @param {number} b - End-Wert
     * @param {number} t - Progress (0-1)
     * @returns {number}
     */
    lerp(a, b, t) {
        return a + (b - a) * t;
    },

    /**
     * Easing Function: Easeout (schnell → langsam)
     * @param {number} t - Progress (0-1)
     * @returns {number}
     */
    easeOut(t) {
        return 1 - Math.pow(1 - t, 3);
    },

    /**
     * Easing Function: Easeoutquad
     * @param {number} t - Progress (0-1)
     * @returns {number}
     */
    easeOutQuad(t) {
        return 1 - (1 - t) * (1 - t);
    },

    /**
     * Dreht Punkt um Mittelpunkt
     * @param {number} x - Punkt X
     * @param {number} y - Punkt Y
     * @param {number} cx - Mittelpunkt X
     * @param {number} cy - Mittelpunkt Y
     * @param {number} angle - Winkel in Radians
     * @returns {Object} { x, y }
     */
    rotatePoint(x, y, cx, cy, angle) {
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);
        return {
            x: cx + (x - cx) * cos - (y - cy) * sin,
            y: cy + (x - cx) * sin + (y - cy) * cos
        };
    },

    /**
     * Berechnet Winkel zwischen zwei Punkten (in Radians)
     * @param {number} x1
     * @param {number} y1
     * @param {number} x2
     * @param {number} y2
     * @returns {number}
     */
    angleTo(x1, y1, x2, y2) {
        return Math.atan2(y2 - y1, x2 - x1);
    },

    /**
     * Drawt Pfeil zwischen zwei Punkten
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} fromX
     * @param {number} fromY
     * @param {number} toX
     * @param {number} toY
     * @param {number} headlen - Pfeilspitzen-Länge
     * @param {string} color
     */
    drawArrow(ctx, fromX, fromY, toX, toY, headlen = 15, color = '#000') {
        const angle = this.angleTo(fromX, fromY, toX, toY);

        // Linien-Start anpassen (kurz vor Zielknoten)
        const distance = this.distance(fromX, fromY, toX, toY);
        const adjustedTo = {
            x: toX - Math.cos(angle) * (headlen + 5),
            y: toY - Math.sin(angle) * (headlen + 5)
        };

        // Linie zeichnen
        ctx.strokeStyle = color;
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(fromX, fromY);
        ctx.lineTo(adjustedTo.x, adjustedTo.y);
        ctx.stroke();

        // Pfeilspitze zeichnen
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.moveTo(toX, toY);
        ctx.lineTo(toX - headlen * Math.cos(angle - Math.PI / 6), toY - headlen * Math.sin(angle - Math.PI / 6));
        ctx.lineTo(toX - headlen * Math.cos(angle + Math.PI / 6), toY - headlen * Math.sin(angle + Math.PI / 6));
        ctx.closePath();
        ctx.fill();
    },

    /**
     * Drawt zentrierten Text mit optional Hintergrund
     * @param {CanvasRenderingContext2D} ctx
     * @param {string} text
     * @param {number} x
     * @param {number} y
     * @param {Object} options - { font, color, bgColor, padding }
     */
    drawText(ctx, text, x, y, options = {}) {
        const defaults = {
            font: '14px Arial',
            color: '#000',
            bgColor: null,
            padding: 4,
            align: 'center',
            baseline: 'middle'
        };
        const opts = { ...defaults, ...options };

        ctx.font = opts.font;
        ctx.textAlign = opts.align;
        ctx.textBaseline = opts.baseline;

        const metrics = ctx.measureText(text);
        const height = parseInt(opts.font) * 1.2;

        // Hintergrund zeichnen falls angegeben
        if (opts.bgColor) {
            ctx.fillStyle = opts.bgColor;
            ctx.fillRect(
                x - metrics.width / 2 - opts.padding,
                y - height / 2 - opts.padding,
                metrics.width + opts.padding * 2,
                height + opts.padding * 2
            );
        }

        // Text zeichnen
        ctx.fillStyle = opts.color;
        ctx.fillText(text, x, y);
    },

    /**
     * Performance: Debounce für Rendering
     * @param {Function} func
     * @param {number} wait - ms
     * @returns {Function}
     */
    debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    },

    /**
     * Performance: RequestAnimationFrame Wrapper
     * @param {Function} callback
     * @param {number} fps - Optional: target FPS (default 60)
     * @returns {Function}
     */
    throttleAnimationFrame(callback, fps = 60) {
        let lastTime = 0;
        const interval = 1000 / fps;

        return (currentTime) => {
            if (currentTime - lastTime >= interval) {
                callback(currentTime);
                lastTime = currentTime;
            }
        };
    }
};

// Export
if (typeof module !== 'undefined' && module.exports) {
    module.exports = VisualizationUtils;
}