Add Flask-CORS and SocketIO for real-time updates, refactor database handling to use a temporary Flask app; improve error handling with @app.errorhandler decorators.

This commit is contained in:
2025-04-28 15:21:11 +02:00
parent 7a0533ac09
commit 0852ea070b
11 changed files with 1633 additions and 1224 deletions

234
static/js/mindmap.html Normal file
View File

@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interaktive Mindmap</title>
<!-- Cytoscape.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
<!-- Feather Icons (optional) -->
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f9fafb;
color: #111827;
line-height: 1.5;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
}
.header {
background-color: #1f2937;
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 1.5rem;
font-weight: 500;
}
.toolbar {
background-color: #f3f4f6;
padding: 0.75rem;
display: flex;
gap: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.btn {
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: #6b7280;
}
.btn-secondary:hover {
background-color: #4b5563;
}
.btn-danger {
background-color: #ef4444;
}
.btn-danger:hover {
background-color: #dc2626;
}
.search-container {
flex: 1;
display: flex;
margin-left: 1rem;
}
.search-input {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
font-size: 0.875rem;
}
#cy {
flex: 1;
width: 100%;
position: relative;
}
.category-filters {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
padding: 0.75rem;
background-color: #ffffff;
border-bottom: 1px solid #e5e7eb;
}
.category-filter {
border: none;
border-radius: 0.25rem;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
cursor: pointer;
transition: opacity 0.2s;
color: white;
}
.category-filter:not(.active) {
opacity: 0.6;
}
.category-filter:hover:not(.active) {
opacity: 0.8;
}
.footer {
background-color: #f3f4f6;
padding: 0.75rem;
text-align: center;
font-size: 0.75rem;
color: #6b7280;
border-top: 1px solid #e5e7eb;
}
/* Kontextmenü Styling */
#context-menu {
position: absolute;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.25rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#context-menu .menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
}
#context-menu .menu-item:hover {
background-color: #f3f4f6;
}
</style>
</head>
<body>
<div class="container">
<header class="header">
<h1>Interaktive Mindmap</h1>
<div class="search-container">
<input type="text" id="search-mindmap" class="search-input" placeholder="Suchen...">
</div>
</header>
<div class="toolbar">
<button id="addNode" class="btn">
<i data-feather="plus-circle"></i>
Knoten hinzufügen
</button>
<button id="addEdge" class="btn">
<i data-feather="git-branch"></i>
Verbindung erstellen
</button>
<button id="editNode" class="btn btn-secondary">
<i data-feather="edit-2"></i>
Knoten bearbeiten
</button>
<button id="deleteNode" class="btn btn-danger">
<i data-feather="trash-2"></i>
Knoten löschen
</button>
<button id="deleteEdge" class="btn btn-danger">
<i data-feather="scissors"></i>
Verbindung löschen
</button>
<button id="reLayout" class="btn btn-secondary">
<i data-feather="refresh-cw"></i>
Layout neu anordnen
</button>
<button id="exportMindmap" class="btn btn-secondary">
<i data-feather="download"></i>
Exportieren
</button>
</div>
<div id="category-filters" class="category-filters">
<!-- Wird dynamisch befüllt -->
</div>
<div id="cy"></div>
<footer class="footer">
Mindmap-Anwendung © 2023
</footer>
</div>
<!-- Unsere Mindmap JS -->
<script src="../js/mindmap.js"></script>
<!-- Icons initialisieren -->
<script>
document.addEventListener('DOMContentLoaded', () => {
if (typeof feather !== 'undefined') {
feather.replace();
}
});
</script>
</body>
</html>

659
static/js/mindmap.js Normal file
View File

@@ -0,0 +1,659 @@
/**
* Mindmap.js - Interaktive Mind-Map Implementierung
* - Cytoscape.js für Graph-Rendering
* - Fetch API für REST-Zugriffe
* - Socket.IO für Echtzeit-Synchronisation
*/
(async () => {
/* 1. Initialisierung und Grundkonfiguration */
const cy = cytoscape({
container: document.getElementById('cy'),
style: [
{
selector: 'node',
style: {
'label': 'data(name)',
'text-valign': 'center',
'color': '#fff',
'background-color': 'data(color)',
'width': 45,
'height': 45,
'font-size': 11,
'text-outline-width': 1,
'text-outline-color': '#000',
'text-outline-opacity': 0.5,
'text-wrap': 'wrap',
'text-max-width': 80
}
},
{
selector: 'node[icon]',
style: {
'background-image': function(ele) {
return `static/img/icons/${ele.data('icon')}.svg`;
},
'background-width': '60%',
'background-height': '60%',
'background-position-x': '50%',
'background-position-y': '40%',
'text-margin-y': 10
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#888',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'target-arrow-color': '#888'
}
},
{
selector: ':selected',
style: {
'border-width': 3,
'border-color': '#f8f32b'
}
}
],
layout: {
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}
});
/* 2. Hilfs-Funktionen für API-Zugriffe */
const get = endpoint => fetch(endpoint).then(r => r.json());
const post = (endpoint, body) =>
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}).then(r => r.json());
const del = endpoint =>
fetch(endpoint, { method: 'DELETE' }).then(r => r.json());
/* 3. Kategorien laden für Style-Informationen */
let categories = await get('/api/categories');
/* 4. Daten laden und Rendering */
const loadMindmap = async () => {
try {
// Nodes und Beziehungen parallel laden
const [nodes, relationships] = await Promise.all([
get('/api/mind_map_nodes'),
get('/api/node_relationships')
]);
// Graph leeren (für Reload-Fälle)
cy.elements().remove();
// Knoten zum Graph hinzufügen
cy.add(
nodes.map(node => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
return {
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
},
position: node.x && node.y ? { x: node.x, y: node.y } : undefined
};
})
);
// Kanten zum Graph hinzufügen
cy.add(
relationships.map(rel => ({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
}))
);
// Layout anwenden wenn keine Positionsdaten vorhanden
const nodesWithoutPosition = cy.nodes().filter(node =>
!node.position() || (node.position().x === 0 && node.position().y === 0)
);
if (nodesWithoutPosition.length > 0) {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
}
// Tooltip-Funktionalität
cy.nodes().unbind('mouseover').bind('mouseover', (event) => {
const node = event.target;
const description = node.data('description');
if (description) {
const tooltip = document.getElementById('node-tooltip') ||
document.createElement('div');
if (!tooltip.id) {
tooltip.id = 'node-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#333';
tooltip.style.color = '#fff';
tooltip.style.padding = '8px';
tooltip.style.borderRadius = '4px';
tooltip.style.maxWidth = '250px';
tooltip.style.zIndex = 10;
tooltip.style.pointerEvents = 'none';
tooltip.style.transition = 'opacity 0.2s';
tooltip.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
document.body.appendChild(tooltip);
}
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
tooltip.innerHTML = description;
tooltip.style.left = (containerRect.left + renderedPosition.x + 25) + 'px';
tooltip.style.top = (containerRect.top + renderedPosition.y - 15) + 'px';
tooltip.style.opacity = '1';
}
});
cy.nodes().unbind('mouseout').bind('mouseout', () => {
const tooltip = document.getElementById('node-tooltip');
if (tooltip) {
tooltip.style.opacity = '0';
}
});
} catch (error) {
console.error('Fehler beim Laden der Mindmap:', error);
alert('Die Mindmap konnte nicht geladen werden. Bitte prüfen Sie die Konsole für Details.');
}
};
// Initial laden
await loadMindmap();
/* 5. Socket.IO für Echtzeit-Synchronisation */
const socket = io();
socket.on('node_added', async (node) => {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cy.add({
data: {
id: node.id.toString(),
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
}
});
// Layout neu anwenden, wenn nötig
if (!node.x || !node.y) {
cy.layout({ name: 'breadthfirst', directed: true, padding: 30 }).run();
}
});
socket.on('node_updated', (node) => {
const cyNode = cy.$id(node.id.toString());
if (cyNode.length > 0) {
// Kategorie-Informationen für Styling abrufen
const category = categories.find(c => c.id === node.category_id) || {};
cyNode.data({
name: node.name,
description: node.description,
color: node.color_code || category.color_code || '#6b7280',
icon: node.icon || category.icon,
category_id: node.category_id
});
if (node.x && node.y) {
cyNode.position({ x: node.x, y: node.y });
}
}
});
socket.on('node_deleted', (nodeId) => {
const cyNode = cy.$id(nodeId.toString());
if (cyNode.length > 0) {
cy.remove(cyNode);
}
});
socket.on('relationship_added', (rel) => {
cy.add({
data: {
id: `${rel.parent_id}_${rel.child_id}`,
source: rel.parent_id.toString(),
target: rel.child_id.toString()
}
});
});
socket.on('relationship_deleted', (rel) => {
const edgeId = `${rel.parent_id}_${rel.child_id}`;
const cyEdge = cy.$id(edgeId);
if (cyEdge.length > 0) {
cy.remove(cyEdge);
}
});
socket.on('category_updated', async () => {
// Kategorien neu laden
categories = await get('/api/categories');
// Nodes aktualisieren, die diese Kategorie verwenden
cy.nodes().forEach(node => {
const categoryId = node.data('category_id');
if (categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
node.data('color', node.data('color_code') || category.color_code);
node.data('icon', node.data('icon') || category.icon);
}
}
});
});
/* 6. UI-Interaktionen */
// Knoten hinzufügen
const btnAddNode = document.getElementById('addNode');
if (btnAddNode) {
btnAddNode.addEventListener('click', async () => {
const name = prompt('Knotenname eingeben:');
if (!name) return;
const description = prompt('Beschreibung (optional):');
// Kategorie auswählen
let categoryId = null;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
'0'
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten erstellen
await post('/api/mind_map_node', {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Verbindung hinzufügen
const btnAddEdge = document.getElementById('addEdge');
if (btnAddEdge) {
btnAddEdge.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 2) {
alert('Bitte genau zwei Knoten auswählen (Parent → Child)');
return;
}
const [parent, child] = sel.map(node => node.id());
await post('/api/node_relationship', {
parent_id: parent,
child_id: child
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten bearbeiten
const btnEditNode = document.getElementById('editNode');
if (btnEditNode) {
btnEditNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
const node = sel[0];
const nodeData = node.data();
const name = prompt('Knotenname:', nodeData.name);
if (!name) return;
const description = prompt('Beschreibung:', nodeData.description || '');
// Kategorie auswählen
let categoryId = nodeData.category_id;
if (categories.length > 0) {
const categoryOptions = categories.map((c, i) => `${i}: ${c.name}`).join('\n');
const categoryChoice = prompt(
`Kategorie auswählen (Nummer eingeben):\n${categoryOptions}`,
categories.findIndex(c => c.id === categoryId).toString()
);
if (categoryChoice !== null) {
const index = parseInt(categoryChoice, 10);
if (!isNaN(index) && index >= 0 && index < categories.length) {
categoryId = categories[index].id;
}
}
}
// Knoten aktualisieren
await post(`/api/mind_map_node/${nodeData.id}`, {
name,
description,
category_id: categoryId
});
// Darstellung wird durch Socket.IO Event übernommen
});
}
// Knoten löschen
const btnDeleteNode = document.getElementById('deleteNode');
if (btnDeleteNode) {
btnDeleteNode.addEventListener('click', async () => {
const sel = cy.$('node:selected');
if (sel.length !== 1) {
alert('Bitte genau einen Knoten auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
const nodeId = sel[0].id();
await del(`/api/mind_map_node/${nodeId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Verbindung löschen
const btnDeleteEdge = document.getElementById('deleteEdge');
if (btnDeleteEdge) {
btnDeleteEdge.addEventListener('click', async () => {
const sel = cy.$('edge:selected');
if (sel.length !== 1) {
alert('Bitte genau eine Verbindung auswählen');
return;
}
if (confirm('Sind Sie sicher, dass Sie diese Verbindung löschen möchten?')) {
const edge = sel[0];
const parentId = edge.source().id();
const childId = edge.target().id();
await del(`/api/node_relationship/${parentId}/${childId}`);
// Darstellung wird durch Socket.IO Event übernommen
}
});
}
// Layout aktualisieren
const btnReLayout = document.getElementById('reLayout');
if (btnReLayout) {
btnReLayout.addEventListener('click', () => {
cy.layout({
name: 'breadthfirst',
directed: true,
padding: 30,
spacingFactor: 1.2
}).run();
});
}
/* 7. Position speichern bei Drag & Drop */
cy.on('dragfree', 'node', async (e) => {
const node = e.target;
const position = node.position();
await post(`/api/mind_map_node/${node.id()}/position`, {
x: Math.round(position.x),
y: Math.round(position.y)
});
// Andere Benutzer erhalten die Position über den node_updated Event
});
/* 8. Kontextmenü (optional) */
const setupContextMenu = () => {
cy.on('cxttap', 'node', function(e) {
const node = e.target;
const nodeData = node.data();
// Position des Kontextmenüs berechnen
const renderedPosition = node.renderedPosition();
const containerRect = cy.container().getBoundingClientRect();
const menuX = containerRect.left + renderedPosition.x;
const menuY = containerRect.top + renderedPosition.y;
// Kontextmenü erstellen oder aktualisieren
let contextMenu = document.getElementById('context-menu');
if (!contextMenu) {
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = '#fff';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.borderRadius = '4px';
contextMenu.style.padding = '5px 0';
contextMenu.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = 1000;
document.body.appendChild(contextMenu);
}
// Menüinhalte
contextMenu.innerHTML = `
<div class="menu-item" data-action="edit">Knoten bearbeiten</div>
<div class="menu-item" data-action="connect">Verbindung erstellen</div>
<div class="menu-item" data-action="delete">Knoten löschen</div>
`;
// Styling für Menüpunkte
const menuItems = contextMenu.querySelectorAll('.menu-item');
menuItems.forEach(item => {
item.style.padding = '8px 20px';
item.style.cursor = 'pointer';
item.style.fontSize = '14px';
item.addEventListener('mouseover', function() {
this.style.backgroundColor = '#f0f0f0';
});
item.addEventListener('mouseout', function() {
this.style.backgroundColor = 'transparent';
});
// Event-Handler
item.addEventListener('click', async function() {
const action = this.getAttribute('data-action');
switch(action) {
case 'edit':
// Knoten bearbeiten (gleiche Logik wie beim Edit-Button)
const name = prompt('Knotenname:', nodeData.name);
if (name) {
const description = prompt('Beschreibung:', nodeData.description || '');
await post(`/api/mind_map_node/${nodeData.id}`, { name, description });
}
break;
case 'connect':
// Modus zum Verbinden aktivieren
cy.nodes().unselect();
node.select();
alert('Wählen Sie nun einen zweiten Knoten aus, um eine Verbindung zu erstellen');
break;
case 'delete':
if (confirm('Sind Sie sicher, dass Sie diesen Knoten löschen möchten?')) {
await del(`/api/mind_map_node/${nodeData.id}`);
}
break;
}
// Menü schließen
contextMenu.style.display = 'none';
});
});
// Menü positionieren und anzeigen
contextMenu.style.left = menuX + 'px';
contextMenu.style.top = menuY + 'px';
contextMenu.style.display = 'block';
// Event-Listener zum Schließen des Menüs
const closeMenu = function() {
if (contextMenu) {
contextMenu.style.display = 'none';
}
document.removeEventListener('click', closeMenu);
};
// Verzögerung, um den aktuellen Click nicht zu erfassen
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 0);
e.preventDefault();
});
};
// Kontextmenü aktivieren (optional)
// setupContextMenu();
/* 9. Export-Funktion (optional) */
const btnExport = document.getElementById('exportMindmap');
if (btnExport) {
btnExport.addEventListener('click', () => {
const elements = cy.json().elements;
const exportData = {
nodes: elements.nodes.map(n => ({
id: n.data.id,
name: n.data.name,
description: n.data.description,
category_id: n.data.category_id,
x: Math.round(n.position?.x || 0),
y: Math.round(n.position?.y || 0)
})),
relationships: elements.edges.map(e => ({
parent_id: e.data.source,
child_id: e.data.target
}))
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'mindmap_export.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
/* 10. Filter-Funktion nach Kategorien (optional) */
const setupCategoryFilters = () => {
const filterContainer = document.getElementById('category-filters');
if (!filterContainer || !categories.length) return;
filterContainer.innerHTML = '';
// "Alle anzeigen" Option
const allBtn = document.createElement('button');
allBtn.innerText = 'Alle Kategorien';
allBtn.className = 'category-filter active';
allBtn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
allBtn.classList.add('active');
cy.nodes().removeClass('filtered').show();
cy.edges().show();
};
filterContainer.appendChild(allBtn);
// Filter-Button pro Kategorie
categories.forEach(category => {
const btn = document.createElement('button');
btn.innerText = category.name;
btn.className = 'category-filter';
btn.style.backgroundColor = category.color_code;
btn.style.color = '#fff';
btn.onclick = () => {
document.querySelectorAll('.category-filter').forEach(btn => btn.classList.remove('active'));
btn.classList.add('active');
const matchingNodes = cy.nodes().filter(node => node.data('category_id') === category.id);
cy.nodes().addClass('filtered').hide();
matchingNodes.removeClass('filtered').show();
// Verbindungen zu/von diesen Knoten anzeigen
cy.edges().hide();
matchingNodes.connectedEdges().show();
};
filterContainer.appendChild(btn);
});
};
// Filter-Funktionalität aktivieren (optional)
// setupCategoryFilters();
/* 11. Suchfunktion (optional) */
const searchInput = document.getElementById('search-mindmap');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
if (!searchTerm) {
cy.nodes().removeClass('search-hidden').show();
cy.edges().show();
return;
}
cy.nodes().forEach(node => {
const name = node.data('name').toLowerCase();
const description = (node.data('description') || '').toLowerCase();
if (name.includes(searchTerm) || description.includes(searchTerm)) {
node.removeClass('search-hidden').show();
node.connectedEdges().show();
} else {
node.addClass('search-hidden').hide();
// Kanten nur verstecken, wenn beide verbundenen Knoten versteckt sind
node.connectedEdges().forEach(edge => {
const otherNode = edge.source().id() === node.id() ? edge.target() : edge.source();
if (otherNode.hasClass('search-hidden')) {
edge.hide();
}
});
}
});
});
}
console.log('Mindmap erfolgreich initialisiert');
})();

View File

@@ -69,21 +69,22 @@ class NeuralNetworkBackground {
// Konfigurationsobjekt für subtilere, sanftere Neuronen
this.config = {
nodeCount: 60, // Weniger Knoten für bessere Leistung und subtileres Aussehen
nodeSize: 2.8, // Kleinere Knoten für dezenteres Erscheinungsbild
nodeVariation: 0.6, // Weniger Varianz für gleichmäßigeres Erscheinungsbild
connectionDistance: 220, // Etwas geringere Verbindungsdistanz
connectionOpacity: 0.15, // Transparentere Verbindungen
nodeCount: 45, // Weniger Knoten für bessere Leistung und subtileres Aussehen
nodeSize: 3.5, // Größere Knoten für bessere Sichtbarkeit
nodeVariation: 0.5, // Weniger Varianz für gleichmäßigeres Erscheinungsbild
connectionDistance: 250, // Größere Verbindungsdistanz
connectionOpacity: 0.22, // Deutlichere Verbindungen
animationSpeed: 0.02, // Langsamere Animation für sanftere Bewegung
pulseSpeed: 0.002, // Langsameres Pulsieren für subtilere Animation
flowSpeed: 0.6, // Langsamer für sanftere Animation
flowDensity: 0.002, // Deutlich weniger Blitze für subtileres Erscheinungsbild
flowSpeed: 0.6, // Langsamer für bessere Sichtbarkeit
flowDensity: 0.005, // Mehr Blitze gleichzeitig erzeugen
flowLength: 0.12, // Kürzere Blitze für dezentere Effekte
maxConnections: 3, // Weniger Verbindungen für aufgeräumteres Erscheinungsbild
clusteringFactor: 0.4, // Moderate Clustering-Stärke
linesFadeDuration: 3500, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms)
linesWidth: 0.6, // Dünnere unterliegende Linien
linesOpacity: 0.25 // Geringere Opazität für Linien
maxConnections: 4, // Mehr Verbindungen pro Neuron
clusteringFactor: 0.45, // Stärkeres Clustering
linesFadeDuration: 4000, // Längere Dauer für sanfteres Ein-/Ausblenden von Linien (ms)
linesWidth: 0.9, // Dickere unterliegende Linien für bessere Sichtbarkeit
linesOpacity: 0.35, // Höhere Opazität für Linien
maxFlowCount: 10 // Maximale Anzahl gleichzeitiger Flüsse
};
// Initialize
@@ -373,11 +374,10 @@ class NeuralNetworkBackground {
const height = this.canvas.height / (window.devicePixelRatio || 1);
const now = Date.now();
// Simulate neural firing with reduced activity
// Setze zunächst alle Neuronen auf inaktiv
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
// Update pulse phase for smoother animation
const node = this.nodes[i];
node.pulsePhase += this.config.pulseSpeed * (1 + (node.connections.length * 0.04));
// Animate node position with gentler movement
@@ -394,57 +394,77 @@ class NeuralNetworkBackground {
node.y = Math.max(0, Math.min(height, node.y));
}
// Check if node should fire based on reduced firing rate
if (now - node.lastFired > node.firingRate * 1.3) { // 30% langsamere Feuerrate
// Setze alle Knoten standardmäßig auf inaktiv
node.isActive = false;
}
// Aktiviere Neuronen basierend auf aktiven Flows
for (const flow of this.flows) {
// Aktiviere den Quellknoten (der Flow geht von ihm aus)
if (flow.sourceNodeIdx !== undefined) {
this.nodes[flow.sourceNodeIdx].isActive = true;
this.nodes[flow.sourceNodeIdx].lastFired = now;
}
// Aktiviere den Zielknoten nur, wenn der Flow weit genug fortgeschritten ist
if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) {
this.nodes[flow.targetNodeIdx].isActive = true;
this.nodes[flow.targetNodeIdx].lastFired = now;
}
}
// Zufällig neue Flows zwischen Knoten initiieren
if (Math.random() < 0.02) { // 2% Chance in jedem Frame
const randomNodeIdx = Math.floor(Math.random() * this.nodes.length);
const node = this.nodes[randomNodeIdx];
// Nur aktivieren, wenn Knoten Verbindungen hat
if (node.connections.length > 0) {
node.isActive = true;
node.lastFired = now;
node.activationTime = now; // Track when activation started
// Activate connected nodes with probability based on connection strength
for (const connIndex of node.connections) {
// Find the connection
const conn = this.connections.find(c =>
(c.from === i && c.to === connIndex) || (c.from === connIndex && c.to === i)
);
// Wähle eine zufällige Verbindung dieses Knotens
const randomConnIdx = Math.floor(Math.random() * node.connections.length);
const connectedNodeIdx = node.connections[randomConnIdx];
// Finde die entsprechende Verbindung
const conn = this.connections.find(c =>
(c.from === randomNodeIdx && c.to === connectedNodeIdx) ||
(c.from === connectedNodeIdx && c.to === randomNodeIdx)
);
if (conn) {
// Markiere die Verbindung als kürzlich aktiviert
conn.lastActivated = now;
if (conn) {
// Mark connection as recently activated
conn.lastActivated = now;
// Stelle sicher, dass die Verbindung sichtbar bleibt
if (conn.fadeState === 'out') {
conn.fadeState = 'visible';
conn.fadeStartTime = now;
}
// Verbindung soll schneller aufgebaut werden
if (conn.progress < 1) {
conn.buildSpeed = 0.015 + Math.random() * 0.01;
}
// Erstelle einen neuen Flow, wenn nicht zu viele existieren
if (this.flows.length < this.config.maxFlowCount) {
// Bestimme die Richtung (vom aktivierten Knoten weg)
const direction = conn.from === randomNodeIdx;
// Wenn eine Verbindung aktiviert wird, verlängere ggf. ihre Sichtbarkeit
if (conn.fadeState === 'out') {
conn.fadeState = 'visible';
conn.fadeStartTime = now;
}
// Verbindung soll schneller aufgebaut werden, wenn ein Neuron feuert
if (conn.progress < 1) {
conn.buildSpeed = 0.015 + Math.random() * 0.01; // Schnellerer Aufbau während der Aktivierung
}
// Reduzierte Wahrscheinlichkeit für neue Flows
if (this.flows.length < 4 && Math.random() < conn.strength * 0.5) { // Reduzierte Wahrscheinlichkeit
this.flows.push({
connection: conn,
progress: 0,
direction: conn.from === i, // Flow from activated node
length: this.config.flowLength + Math.random() * 0.05, // Geringere Variation
intensity: 0.5 + Math.random() * 0.3, // Geringere Intensität für subtilere Darstellung
creationTime: now,
totalDuration: 1000 + Math.random() * 600 // Längere Dauer für sanftere Animation
});
}
// Probability for connected node to activate
if (Math.random() < conn.strength * 0.5) {
this.nodes[connIndex].isActive = true;
this.nodes[connIndex].activationTime = now;
this.nodes[connIndex].lastFired = now - Math.random() * 500; // Slight variation
}
this.flows.push({
connection: conn,
progress: 0,
direction: direction,
length: this.config.flowLength + Math.random() * 0.05,
creationTime: now,
totalDuration: 1000 + Math.random() * 600,
sourceNodeIdx: direction ? conn.from : conn.to,
targetNodeIdx: direction ? conn.to : conn.from
});
}
}
} else if (now - node.lastFired > 400) { // Deactivate after longer period
node.isActive = false;
}
}
@@ -466,60 +486,44 @@ class NeuralNetworkBackground {
connection.fadeState = 'out';
connection.fadeStartTime = now;
connection.fadeProgress = 1.0;
// Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann
if (Math.random() < 0.7) {
connection.progress = 0;
}
}
} else if (connection.fadeState === 'out') {
// Ausblenden
connection.fadeProgress = Math.max(0.0, 1.0 - (elapsedTime / connection.fadeTotalDuration));
if (connection.fadeProgress <= 0.0) {
// Setze Verbindung zurück, damit sie wieder eingeblendet werden kann
if (Math.random() < 0.4) { // 40% Chance, direkt wieder einzublenden
connection.fadeState = 'in';
connection.fadeStartTime = now;
connection.fadeProgress = 0.0;
connection.visibleDuration = 10000 + Math.random() * 15000; // Neue Dauer generieren
// Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann
connection.progress = 0;
} else {
// Kurze Pause, bevor die Verbindung wieder erscheint
connection.fadeState = 'hidden';
connection.fadeStartTime = now;
connection.hiddenDuration = 3000 + Math.random() * 7000;
// Setze den Fortschritt zurück, damit die Verbindung neu aufgebaut werden kann
connection.progress = 0;
}
}
} else if (connection.fadeState === 'hidden') {
// Verbindung ist unsichtbar, warte auf Wiedereinblendung
if (elapsedTime > connection.hiddenDuration) {
// Ausblenden, aber nie komplett verschwinden
connection.fadeProgress = Math.max(0.1, 1.0 - (elapsedTime / connection.fadeTotalDuration));
// Verbindungen bleiben immer minimal sichtbar (nie komplett unsichtbar)
if (connection.fadeProgress <= 0.1) {
// Statt Verbindung komplett zu verstecken, setzen wir sie zurück auf "in"
connection.fadeState = 'in';
connection.fadeStartTime = now;
connection.fadeProgress = 0.0;
// Verbindung wird komplett neu aufgebaut
connection.progress = 0;
connection.fadeProgress = 0.1; // Minimal sichtbar bleiben
connection.visibleDuration = 15000 + Math.random() * 20000; // Längere Sichtbarkeit
}
} else if (connection.fadeState === 'hidden') {
// Keine Verbindungen mehr verstecken, stattdessen immer wieder einblenden
connection.fadeState = 'in';
connection.fadeStartTime = now;
connection.fadeProgress = 0.1;
}
// Animierter Verbindungsaufbau: progress inkrementieren, aber nur wenn aktiv
// Verbindungen immer vollständig aufbauen und nicht zurücksetzen
if (connection.progress < 1) {
// Verbindung wird nur aufgebaut, wenn sie gerade aktiv ist oder ein Blitz sie aufbaut
const buildingSpeed = connection.buildSpeed || 0.002; // Langsamer Standard-Aufbau
// Konstante Aufbaugeschwindigkeit, unabhängig vom Status
const baseBuildSpeed = 0.003;
let buildSpeed = connection.buildSpeed || baseBuildSpeed;
// Bau die Verbindung auf, wenn sie kürzlich aktiviert wurde
// Wenn kürzlich aktiviert, schneller aufbauen
if (now - connection.lastActivated < 2000) {
connection.progress += buildingSpeed;
if (connection.progress > 1) connection.progress = 1;
buildSpeed = Math.max(buildSpeed, 0.006);
}
// Zurücksetzen der Aufbaugeschwindigkeit
connection.buildSpeed = 0;
connection.progress += buildSpeed;
if (connection.progress > 1) {
connection.progress = 1;
// Zurücksetzen der Aufbaugeschwindigkeit
connection.buildSpeed = 0;
}
}
}
@@ -527,7 +531,7 @@ class NeuralNetworkBackground {
this.updateFlows(now);
// Seltener neue Flows erstellen
if (Math.random() < this.config.flowDensity * 0.8 && this.flows.length < 4) { // Reduzierte Kapazität und Rate
if (Math.random() < this.config.flowDensity && this.flows.length < this.config.maxFlowCount) {
this.createNewFlow(now);
}
@@ -560,6 +564,22 @@ class NeuralNetworkBackground {
// Update flow progress
flow.progress += this.config.flowSpeed / flow.connection.distance;
// Aktiviere Quell- und Zielknoten basierend auf Flow-Fortschritt
if (flow.sourceNodeIdx !== undefined) {
// Quellknoten immer aktivieren, solange der Flow aktiv ist
this.nodes[flow.sourceNodeIdx].isActive = true;
this.nodes[flow.sourceNodeIdx].lastFired = now;
}
// Zielknoten erst aktivieren, wenn der Flow ihn erreicht hat
if (flow.targetNodeIdx !== undefined && flow.progress > 0.9) {
this.nodes[flow.targetNodeIdx].isActive = true;
this.nodes[flow.targetNodeIdx].lastFired = now;
}
// Stellen Sie sicher, dass die Verbindung aktiv bleibt
flow.connection.lastActivated = now;
// Remove completed or expired flows
if (flow.progress > 1.0 || flowProgress >= 1.0) {
this.flows.splice(i, 1);
@@ -1111,11 +1131,11 @@ class NeuralNetworkBackground {
// Weniger Funken mit geringerer Vibration
const sparks = this.generateSparkPoints(zigzag, 4 + Math.floor(Math.random() * 2));
// Dezenteres Funkenlicht mit Ein-/Ausblendeffekt
const sparkBaseOpacity = this.isDarkMode ? 0.65 : 0.55;
// Intensiveres Funkenlicht mit dynamischem Ein-/Ausblendeffekt
const sparkBaseOpacity = this.isDarkMode ? 0.75 : 0.65;
const sparkBaseColor = this.isDarkMode
? `rgba(220, 235, 245, ${sparkBaseOpacity * fadeFactor})`
: `rgba(180, 220, 245, ${sparkBaseOpacity * fadeFactor})`;
? `rgba(230, 240, 250, ${sparkBaseOpacity * fadeFactor})`
: `rgba(190, 230, 250, ${sparkBaseOpacity * fadeFactor})`;
for (const spark of sparks) {
this.ctx.beginPath();
@@ -1147,34 +1167,36 @@ class NeuralNetworkBackground {
this.ctx.fill();
}
// Dezenterer Fortschrittseffekt an der Spitze des Blitzes
if (endProgress >= connProgress - 0.05 && connProgress < 0.95) {
// Deutlicherer und länger anhaltender Fortschrittseffekt an der Spitze des Blitzes
if (endProgress >= connProgress - 0.1 && connProgress < 0.98) {
const tipGlow = this.ctx.createRadialGradient(
p2.x, p2.y, 0,
p2.x, p2.y, 6
p2.x, p2.y, 10
);
tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.7 * fadeFactor})`);
tipGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.85 * fadeFactor})`);
tipGlow.addColorStop(0.5, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor})`);
tipGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`);
this.ctx.fillStyle = tipGlow;
this.ctx.beginPath();
this.ctx.arc(p2.x, p2.y, 6, 0, Math.PI * 2);
this.ctx.arc(p2.x, p2.y, 10, 0, Math.PI * 2);
this.ctx.fill();
}
// Sanftere Start- und Endblitz-Fades
if (startProgress < 0.1) {
const startFade = startProgress / 0.1; // 0 bis 1
// Verstärkter Start- und Endblitz-Fade mit längerer Sichtbarkeit
if (startProgress < 0.15) {
const startFade = startProgress / 0.15; // 0 bis 1
const startGlow = this.ctx.createRadialGradient(
p1.x, p1.y, 0,
p1.x, p1.y, 8 * startFade
p1.x, p1.y, 12 * startFade
);
startGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.4 * fadeFactor * startFade})`);
startGlow.addColorStop(0, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.6 * fadeFactor * startFade})`);
startGlow.addColorStop(0.7, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.3 * fadeFactor * startFade})`);
startGlow.addColorStop(1, `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, 0)`);
this.ctx.fillStyle = startGlow;
this.ctx.beginPath();
this.ctx.arc(p1.x, p1.y, 8 * startFade, 0, Math.PI * 2);
this.ctx.arc(p1.x, p1.y, 12 * startFade, 0, Math.PI * 2);
this.ctx.fill();
}
@@ -1259,11 +1281,11 @@ class NeuralNetworkBackground {
return points;
}
// Hilfsfunktion: Erzeuge dezentere Funkenpunkte mit gemäßigter Verteilung
generateSparkPoints(zigzag, sparkCount = 4) {
// Hilfsfunktion: Erzeuge intensivere Funkenpunkte mit dynamischer Verteilung
generateSparkPoints(zigzag, sparkCount = 15) {
const sparks = [];
// Weniger Funken
const actualSparkCount = Math.min(sparkCount, zigzag.length);
// Mehr Funken für intensiveren Effekt
const actualSparkCount = Math.min(sparkCount, zigzag.length * 2);
// Funken an zufälligen Stellen entlang des Blitzes
for (let i = 0; i < actualSparkCount; i++) {
@@ -1280,15 +1302,31 @@ class NeuralNetworkBackground {
const x = zigzag[segIndex].x + dx * t;
const y = zigzag[segIndex].y + dy * t;
// Rechtwinkliger Versatz vom Segment (sanftere Verteilung)
const offsetAngle = segmentAngle + Math.PI/2;
const offsetDistance = Math.random() * 4 - 2; // Geringerer Offset für dezentere Funken
// Dynamischer Versatz für intensivere Funken
const offsetAngle = segmentAngle + (Math.random() * Math.PI - Math.PI/2);
const offsetDistance = Math.random() * 8 - 4; // Größerer Offset für dramatischere Funken
// Zufällige Größe für variierende Intensität
const baseSize = 3.5 + Math.random() * 3.5;
const sizeVariation = Math.random() * 2.5;
sparks.push({
x: x + Math.cos(offsetAngle) * offsetDistance,
y: y + Math.sin(offsetAngle) * offsetDistance,
size: 1 + Math.random() * 1.5 // Kleinere Funkengröße für subtilere Effekte
size: baseSize + sizeVariation // Größere und variablere Funkengröße
});
// Zusätzliche kleinere Funken in der Nähe für einen intensiveren Effekt
if (Math.random() < 0.4) { // 40% Chance für zusätzliche Funken
const subSparkAngle = offsetAngle + (Math.random() * Math.PI/2 - Math.PI/4);
const subDistance = offsetDistance * (0.4 + Math.random() * 0.6);
sparks.push({
x: x + Math.cos(subSparkAngle) * subDistance,
y: y + Math.sin(subSparkAngle) * subDistance,
size: (baseSize + sizeVariation) * 0.6 // Kleinere Größe für sekundäre Funken
});
}
}
return sparks;