/** * D3.js Erweiterungen für verbesserte Mindmap-Funktionalität * Diese Datei enthält zusätzliche Hilfsfunktionen und Erweiterungen für D3.js */ class D3Extensions { /** * Erstellt einen verbesserten radialen Farbverlauf * @param {Object} defs - Das D3 defs Element * @param {string} id - ID für den Gradienten * @param {string} baseColor - Grundfarbe in hexadezimal oder RGB * @returns {Object} - Das erstellte Gradient-Element */ static createEnhancedRadialGradient(defs, id, baseColor) { // Farben berechnen const d3Color = d3.color(baseColor); const lightColor = d3Color.brighter(0.7); const darkColor = d3Color.darker(0.3); const midColor = d3Color; // Gradient erstellen const gradient = defs.append('radialGradient') .attr('id', id) .attr('cx', '30%') .attr('cy', '30%') .attr('r', '70%'); // Farbstops hinzufügen für realistischeren Verlauf gradient.append('stop') .attr('offset', '0%') .attr('stop-color', lightColor.formatHex()); gradient.append('stop') .attr('offset', '50%') .attr('stop-color', midColor.formatHex()); gradient.append('stop') .attr('offset', '100%') .attr('stop-color', darkColor.formatHex()); return gradient; } /** * Erstellt einen Glüheffekt-Filter * @param {Object} defs - D3-Referenz auf den defs-Bereich * @param {String} id - ID des Filters * @param {String} color - Farbe des Glüheffekts (Hex-Code) * @param {Number} strength - Stärke des Glüheffekts * @returns {Object} D3-Referenz auf den erstellten Filter */ static createGlowFilter(defs, id, color = '#b38fff', strength = 5) { const filter = defs.append('filter') .attr('id', id) .attr('x', '-50%') .attr('y', '-50%') .attr('width', '200%') .attr('height', '200%'); // Unschärfe-Effekt filter.append('feGaussianBlur') .attr('in', 'SourceGraphic') .attr('stdDeviation', strength) .attr('result', 'blur'); // Farbverstärkung für den Glüheffekt filter.append('feColorMatrix') .attr('in', 'blur') .attr('type', 'matrix') .attr('values', '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 -7') .attr('result', 'glow'); // Farbflut mit der angegebenen Farbe filter.append('feFlood') .attr('flood-color', color) .attr('flood-opacity', '0.7') .attr('result', 'color'); // Zusammensetzen des Glüheffekts mit der Farbe filter.append('feComposite') .attr('in', 'color') .attr('in2', 'glow') .attr('operator', 'in') .attr('result', 'glow-color'); // Zusammenfügen aller Ebenen const feMerge = filter.append('feMerge'); feMerge.append('feMergeNode') .attr('in', 'glow-color'); feMerge.append('feMergeNode') .attr('in', 'SourceGraphic'); return filter; } /** * Berechnet eine konsistente Farbe aus einem String * @param {string} str - Eingabestring * @returns {string} - Generierte Farbe als Hex-String */ static stringToColor(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } // Basis-Farbpalette für konsistente Farben const colorPalette = [ "#4299E1", // Blau "#9F7AEA", // Lila "#ED64A6", // Pink "#48BB78", // Grün "#ECC94B", // Gelb "#F56565", // Rot "#38B2AC", // Türkis "#ED8936", // Orange "#667EEA", // Indigo ]; // Farbe aus der Palette wählen basierend auf dem Hash const colorIndex = Math.abs(hash) % colorPalette.length; return colorPalette[colorIndex]; } /** * Erstellt einen Schatteneffekt-Filter * @param {Object} defs - D3-Referenz auf den defs-Bereich * @param {String} id - ID des Filters * @returns {Object} D3-Referenz auf den erstellten Filter */ static createShadowFilter(defs, id) { const filter = defs.append('filter') .attr('id', id) .attr('x', '-50%') .attr('y', '-50%') .attr('width', '200%') .attr('height', '200%'); // Einfacher Schlagschatten filter.append('feDropShadow') .attr('dx', 0) .attr('dy', 4) .attr('stdDeviation', 4) .attr('flood-color', 'rgba(0, 0, 0, 0.3)'); return filter; } /** * Erstellt einen Glasmorphismus-Effekt-Filter * @param {Object} defs - D3-Referenz auf den defs-Bereich * @param {String} id - ID des Filters * @returns {Object} D3-Referenz auf den erstellten Filter */ static createGlassMorphismFilter(defs, id) { const filter = defs.append('filter') .attr('id', id) .attr('x', '-50%') .attr('y', '-50%') .attr('width', '200%') .attr('height', '200%'); // Hintergrund-Unschärfe für den Glaseffekt filter.append('feGaussianBlur') .attr('in', 'SourceGraphic') .attr('stdDeviation', 8) .attr('result', 'blur'); // Hellere Farbe für den Glaseffekt filter.append('feColorMatrix') .attr('in', 'blur') .attr('type', 'matrix') .attr('values', '1 0 0 0 0.1 0 1 0 0 0.1 0 0 1 0 0.1 0 0 0 0.6 0') .attr('result', 'glass'); // Überlagerung mit dem Original const feMerge = filter.append('feMerge'); feMerge.append('feMergeNode') .attr('in', 'glass'); feMerge.append('feMergeNode') .attr('in', 'SourceGraphic'); return filter; } /** * Erstellt einen verstärkten Glasmorphismus-Effekt mit Farbverlauf * @param {Object} defs - D3-Referenz auf den defs-Bereich * @param {String} id - ID des Filters * @param {String} color1 - Erste Farbe des Verlaufs (Hex-Code) * @param {String} color2 - Zweite Farbe des Verlaufs (Hex-Code) * @returns {Object} D3-Referenz auf den erstellten Filter */ static createEnhancedGlassMorphismFilter(defs, id, color1 = '#b38fff', color2 = '#58a9ff') { // Farbverlauf für den Glaseffekt definieren const gradientId = `gradient-${id}`; const gradient = defs.append('linearGradient') .attr('id', gradientId) .attr('x1', '0%') .attr('y1', '0%') .attr('x2', '100%') .attr('y2', '100%'); gradient.append('stop') .attr('offset', '0%') .attr('stop-color', color1) .attr('stop-opacity', '0.3'); gradient.append('stop') .attr('offset', '100%') .attr('stop-color', color2) .attr('stop-opacity', '0.3'); // Filter erstellen const filter = defs.append('filter') .attr('id', id) .attr('x', '-50%') .attr('y', '-50%') .attr('width', '200%') .attr('height', '200%'); // Hintergrund-Unschärfe filter.append('feGaussianBlur') .attr('in', 'SourceGraphic') .attr('stdDeviation', 6) .attr('result', 'blur'); // Farbverlauf einfügen const feImage = filter.append('feImage') .attr('xlink:href', `#${gradientId}`) .attr('result', 'gradient') .attr('x', '0%') .attr('y', '0%') .attr('width', '100%') .attr('height', '100%') .attr('preserveAspectRatio', 'none'); // Zusammenfügen aller Ebenen const feMerge = filter.append('feMerge'); feMerge.append('feMergeNode') .attr('in', 'blur'); feMerge.append('feMergeNode') .attr('in', 'gradient'); feMerge.append('feMergeNode') .attr('in', 'SourceGraphic'); return filter; } /** * Erstellt einen 3D-Glaseffekt mit verbesserter Tiefe und Reflexionen * @param {Object} defs - D3-Referenz auf den defs-Bereich * @param {String} id - ID des Filters * @returns {Object} D3-Referenz auf den erstellten Filter */ static create3DGlassEffect(defs, id) { const filter = defs.append('filter') .attr('id', id) .attr('x', '-50%') .attr('y', '-50%') .attr('width', '200%') .attr('height', '200%'); // Farbmatrix für Transparenz filter.append('feColorMatrix') .attr('type', 'matrix') .attr('in', 'SourceGraphic') .attr('values', '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.7 0') .attr('result', 'transparent'); // Hintergrund-Unschärfe für Tiefe filter.append('feGaussianBlur') .attr('in', 'transparent') .attr('stdDeviation', '4') .attr('result', 'blurred'); // Lichtquelle und Schattierung hinzufügen const lightSource = filter.append('feSpecularLighting') .attr('in', 'blurred') .attr('surfaceScale', '6') .attr('specularConstant', '1') .attr('specularExponent', '30') .attr('lighting-color', '#ffffff') .attr('result', 'specular'); lightSource.append('fePointLight') .attr('x', '100') .attr('y', '100') .attr('z', '200'); // Lichtreflexion verstärken filter.append('feComposite') .attr('in', 'specular') .attr('in2', 'SourceGraphic') .attr('operator', 'in') .attr('result', 'specularHighlight'); // Inneren Schatten erzeugen const innerShadow = filter.append('feOffset') .attr('in', 'SourceAlpha') .attr('dx', '0') .attr('dy', '1') .attr('result', 'offsetblur'); innerShadow.append('feGaussianBlur') .attr('in', 'offsetblur') .attr('stdDeviation', '2') .attr('result', 'innerShadow'); filter.append('feComposite') .attr('in', 'innerShadow') .attr('in2', 'SourceGraphic') .attr('operator', 'out') .attr('result', 'innerShadowEffect'); // Schichten kombinieren const feMerge = filter.append('feMerge'); feMerge.append('feMergeNode') .attr('in', 'blurred'); feMerge.append('feMergeNode') .attr('in', 'innerShadowEffect'); feMerge.append('feMergeNode') .attr('in', 'specularHighlight'); feMerge.append('feMergeNode') .attr('in', 'SourceGraphic'); return filter; } /** * Fügt einen Partikelsystem-Effekt für interaktive Knoten hinzu * @param {Object} parent - Das übergeordnete SVG-Element * @param {number} x - X-Koordinate des Zentrums * @param {number} y - Y-Koordinate des Zentrums * @param {string} color - Partikelfarbe (Hex-Code) * @param {number} count - Anzahl der Partikel */ static createParticleEffect(parent, x, y, color = '#b38fff', count = 5) { const particles = []; for (let i = 0; i < count; i++) { const particle = parent.append('circle') .attr('cx', x) .attr('cy', y) .attr('r', 0) .attr('fill', color) .style('opacity', 0.8); particles.push(particle); // Partikel animieren animateParticle(particle); } function animateParticle(particle) { // Zufällige Richtung und Geschwindigkeit const angle = Math.random() * Math.PI * 2; const speed = 1 + Math.random() * 2; const distance = 20 + Math.random() * 30; // Zielposition berechnen const targetX = x + Math.cos(angle) * distance; const targetY = y + Math.sin(angle) * distance; // Animation mit zufälliger Dauer const duration = 1000 + Math.random() * 500; particle .attr('r', 0) .style('opacity', 0.8) .transition() .duration(duration) .attr('cx', targetX) .attr('cy', targetY) .attr('r', 2 + Math.random() * 3) .style('opacity', 0) .on('end', function() { // Partikel entfernen particle.remove(); }); } } /** * Führt eine Pulsanimation auf einem Knoten durch * @param {Object} node - D3-Knoten-Selektion * @returns {void} */ static pulseAnimation(node) { if (!node) return; const circle = node.select('circle'); const originalRadius = parseFloat(circle.attr('r')); const originalFill = circle.attr('fill'); // Pulsanimation circle .transition() .duration(400) .attr('r', originalRadius * 1.3) .attr('fill', '#b38fff') .transition() .duration(400) .attr('r', originalRadius) .attr('fill', originalFill); } /** * Berechnet eine adaptive Schriftgröße basierend auf der Textlänge * @param {string} text - Der anzuzeigende Text * @param {number} maxSize - Maximale Schriftgröße in Pixel * @param {number} minSize - Minimale Schriftgröße in Pixel * @returns {number} - Die berechnete Schriftgröße */ static getAdaptiveFontSize(text, maxSize = 14, minSize = 10) { if (!text) return maxSize; // Linear die Schriftgröße basierend auf der Textlänge anpassen const length = text.length; if (length <= 6) return maxSize; if (length >= 20) return minSize; // Lineare Interpolation const factor = (length - 6) / (20 - 6); return maxSize - factor * (maxSize - minSize); } /** * Fügt einen Pulsierenden Effekt zu einer Selektion hinzu * @param {Object} selection - D3-Selektion * @param {number} duration - Dauer eines Puls-Zyklus in ms * @param {number} minOpacity - Minimale Opazität * @param {number} maxOpacity - Maximale Opazität */ static addPulseEffect(selection, duration = 1500, minOpacity = 0.4, maxOpacity = 0.9) { function pulse() { selection .transition() .duration(duration / 2) .style('opacity', minOpacity) .transition() .duration(duration / 2) .style('opacity', maxOpacity) .on('end', pulse); } // Initialen Stil setzen selection.style('opacity', maxOpacity); // Pulsanimation starten pulse(); } /** * Verarbeitet Daten aus der Datenbank für die Mindmap-Visualisierung * @param {Array} databaseNodes - Knotendaten aus der Datenbank * @param {Array} links - Verbindungsdaten oder null für automatische Extraktion * @returns {Object} Aufbereitete Daten für D3.js */ static processDbNodesForVisualization(databaseNodes, links = null) { // Überprüfe, ob Daten vorhanden sind if (!databaseNodes || databaseNodes.length === 0) { console.warn('Keine Knotendaten zum Verarbeiten vorhanden'); return { nodes: [], links: [] }; } // Knoten mit D3-Kompatiblem Format erstellen const nodes = databaseNodes.map(node => { // Farbgenerierung, falls keine vorhanden const nodeColor = node.color_code || node.color || D3Extensions.stringToColor(node.name || 'default'); return { id: node.id, name: node.name, description: node.description || '', thought_count: node.thought_count || 0, color: nodeColor, // Zusätzliche Attribute category_id: node.category_id, is_public: node.is_public !== undefined ? node.is_public : true, // Position, falls vorhanden x: node.x_position, y: node.y_position, // Größe, falls vorhanden scale: node.scale || 1.0 }; }); // Verbindungen verarbeiten let processedLinks = []; if (links && Array.isArray(links)) { // Verwende übergebene Verbindungen processedLinks = links.map(link => { return { source: link.source, target: link.target, // Zusätzliche Attribute type: link.type || 'default', strength: link.strength || 1 }; }); } else { // Extrahiere Verbindungen aus den Knoten databaseNodes.forEach(node => { if (node.connections && Array.isArray(node.connections)) { node.connections.forEach(conn => { processedLinks.push({ source: node.id, target: conn.target, type: conn.type || 'default', strength: conn.strength || 1 }); }); } }); } return { nodes, links: processedLinks }; } } // Globale Verfügbarkeit sicherstellen window.D3Extensions = D3Extensions;