457 lines
14 KiB
JavaScript
457 lines
14 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
// Globale Verfügbarkeit sicherstellen
|
|
window.D3Extensions = D3Extensions;
|