934 lines
31 KiB
JavaScript
934 lines
31 KiB
JavaScript
/**
|
|
* Neural Network Background Animation
|
|
* Modern, darker, mystical theme using WebGL
|
|
* Subtle flowing network aesthetic
|
|
*/
|
|
|
|
class NeuralNetworkBackground {
|
|
constructor() {
|
|
// Canvas setup
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.id = 'neural-network-background';
|
|
this.canvas.style.position = 'fixed';
|
|
this.canvas.style.top = '0';
|
|
this.canvas.style.left = '0';
|
|
this.canvas.style.width = '100%';
|
|
this.canvas.style.height = '100%';
|
|
this.canvas.style.zIndex = '-10'; // Ensure it's behind content but visible
|
|
this.canvas.style.pointerEvents = 'none';
|
|
this.canvas.style.opacity = '1'; // Force visibility
|
|
|
|
// If canvas already exists, remove it first
|
|
const existingCanvas = document.getElementById('neural-network-background');
|
|
if (existingCanvas) {
|
|
existingCanvas.remove();
|
|
}
|
|
|
|
// Append to body as first child to ensure it's behind everything
|
|
if (document.body.firstChild) {
|
|
document.body.insertBefore(this.canvas, document.body.firstChild);
|
|
} else {
|
|
document.body.appendChild(this.canvas);
|
|
}
|
|
|
|
// WebGL context
|
|
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl');
|
|
if (!this.gl) {
|
|
console.warn('WebGL not supported, falling back to canvas rendering');
|
|
this.gl = null;
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.useWebGL = false;
|
|
} else {
|
|
this.useWebGL = true;
|
|
}
|
|
|
|
// Animation properties
|
|
this.nodes = [];
|
|
this.connections = [];
|
|
this.flows = []; // Flow animations along connections
|
|
this.animationFrameId = null;
|
|
this.isDarkMode = true; // Always use dark mode for the background
|
|
|
|
// Colors - Updated for intense visibility and neural effect
|
|
this.darkModeColors = {
|
|
background: '#030610', // Noch dunklerer Hintergrund für besseren Kontrast
|
|
nodeColor: '#88a5ff', // Hellere, leuchtende Knoten
|
|
nodePulse: '#c0d5ff', // Strahlend helles Pulsieren
|
|
connectionColor: '#5a6ca8', // Hellere, sichtbarere Verbindungen
|
|
flowColor: '#90c8ffee' // Sehr leuchtende, fast undurchsichtige Flüsse
|
|
};
|
|
|
|
this.lightModeColors = {
|
|
background: '#f9fafb',
|
|
nodeColor: '#8c4aff',
|
|
nodePulse: '#ab7cff',
|
|
connectionColor: '#b798ff',
|
|
flowColor: '#d4c5ff'
|
|
};
|
|
|
|
// Config - Drastisch verstärkt für strahlende Animationen und neuronale Vernetzung
|
|
this.config = {
|
|
nodeCount: 120, // Anzahl der Knoten bleibt hoch für Netzwerkstruktur
|
|
nodeSize: 1.1, // Dezenter: kleinere Knoten
|
|
nodeVariation: 0.4, // Weniger Variation für ruhigeres Bild
|
|
connectionDistance: 220, // Unverändert: gute Vernetzung
|
|
connectionOpacity: 0.18, // Deutlich dezentere Verbindungen
|
|
animationSpeed: 0.05, // Ruhigere Bewegung
|
|
pulseSpeed: 0.004, // Ruhigeres Pulsieren
|
|
flowSpeed: 1.2, // Flows schneller für flüssigere Aktivität
|
|
flowDensity: 0.012, // Mehr Flows für sichtbare Aktivität
|
|
flowLength: 0.32, // Flows länger sichtbar
|
|
maxConnections: 5, // Weniger Überlagerung
|
|
clusteringFactor: 0.35 // Mehr Cluster für neuronalen Effekt
|
|
};
|
|
|
|
// Initialize
|
|
this.init();
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', this.resizeCanvas.bind(this));
|
|
document.addEventListener('darkModeToggled', (event) => {
|
|
this.isDarkMode = event.detail.isDark;
|
|
});
|
|
|
|
// Log that the background is initialized
|
|
console.log('Neural Network Background initialized');
|
|
}
|
|
|
|
init() {
|
|
this.resizeCanvas();
|
|
|
|
if (this.useWebGL) {
|
|
this.initWebGL();
|
|
}
|
|
|
|
this.createNodes();
|
|
this.createConnections();
|
|
this.startAnimation();
|
|
}
|
|
|
|
resizeCanvas() {
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight;
|
|
|
|
this.canvas.style.width = width + 'px';
|
|
this.canvas.style.height = height + 'px';
|
|
this.canvas.width = width * pixelRatio;
|
|
this.canvas.height = height * pixelRatio;
|
|
|
|
if (this.useWebGL) {
|
|
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
|
|
} else if (this.ctx) {
|
|
this.ctx.scale(pixelRatio, pixelRatio);
|
|
}
|
|
|
|
// Recalculate node positions after resize
|
|
if (this.nodes.length) {
|
|
this.createNodes();
|
|
this.createConnections();
|
|
}
|
|
}
|
|
|
|
initWebGL() {
|
|
// Vertex shader
|
|
const vsSource = `
|
|
attribute vec2 aVertexPosition;
|
|
attribute float aPointSize;
|
|
uniform vec2 uResolution;
|
|
|
|
void main() {
|
|
// Convert from pixel to clip space
|
|
vec2 position = (aVertexPosition / uResolution) * 2.0 - 1.0;
|
|
// Flip Y coordinate
|
|
position.y = -position.y;
|
|
|
|
gl_Position = vec4(position, 0, 1);
|
|
gl_PointSize = aPointSize;
|
|
}
|
|
`;
|
|
|
|
// Fragment shader - Softer glow effect
|
|
const fsSource = `
|
|
precision mediump float;
|
|
uniform vec4 uColor;
|
|
|
|
void main() {
|
|
float distance = length(gl_PointCoord - vec2(0.5, 0.5));
|
|
|
|
// Softer glow with smoother falloff
|
|
float alpha = 1.0 - smoothstep(0.1, 0.5, distance);
|
|
alpha = pow(alpha, 1.5); // Make the glow even softer
|
|
|
|
gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
|
|
}
|
|
`;
|
|
|
|
// Initialize shaders
|
|
const vertexShader = this.loadShader(this.gl.VERTEX_SHADER, vsSource);
|
|
const fragmentShader = this.loadShader(this.gl.FRAGMENT_SHADER, fsSource);
|
|
|
|
// Create shader program
|
|
this.shaderProgram = this.gl.createProgram();
|
|
this.gl.attachShader(this.shaderProgram, vertexShader);
|
|
this.gl.attachShader(this.shaderProgram, fragmentShader);
|
|
this.gl.linkProgram(this.shaderProgram);
|
|
|
|
if (!this.gl.getProgramParameter(this.shaderProgram, this.gl.LINK_STATUS)) {
|
|
console.error('Unable to initialize the shader program: ' +
|
|
this.gl.getProgramInfoLog(this.shaderProgram));
|
|
return;
|
|
}
|
|
|
|
// Get attribute and uniform locations
|
|
this.programInfo = {
|
|
program: this.shaderProgram,
|
|
attribLocations: {
|
|
vertexPosition: this.gl.getAttribLocation(this.shaderProgram, 'aVertexPosition'),
|
|
pointSize: this.gl.getAttribLocation(this.shaderProgram, 'aPointSize')
|
|
},
|
|
uniformLocations: {
|
|
resolution: this.gl.getUniformLocation(this.shaderProgram, 'uResolution'),
|
|
color: this.gl.getUniformLocation(this.shaderProgram, 'uColor')
|
|
}
|
|
};
|
|
|
|
// Create buffers
|
|
this.positionBuffer = this.gl.createBuffer();
|
|
this.sizeBuffer = this.gl.createBuffer();
|
|
|
|
// Set clear color for WebGL context
|
|
const bgColor = this.hexToRgb(this.darkModeColors.background);
|
|
|
|
this.gl.clearColor(bgColor.r/255, bgColor.g/255, bgColor.b/255, 1.0);
|
|
}
|
|
|
|
loadShader(type, source) {
|
|
const shader = this.gl.createShader(type);
|
|
this.gl.shaderSource(shader, source);
|
|
this.gl.compileShader(shader);
|
|
|
|
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
console.error('An error occurred compiling the shaders: ' +
|
|
this.gl.getShaderInfoLog(shader));
|
|
this.gl.deleteShader(shader);
|
|
return null;
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
createNodes() {
|
|
this.nodes = [];
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
// Erstelle Cluster-Zentren für neuronale Netzwerkmuster
|
|
const clusterCount = Math.floor(5 + Math.random() * 4); // 5-8 Cluster
|
|
const clusters = [];
|
|
|
|
for (let i = 0; i < clusterCount; i++) {
|
|
clusters.push({
|
|
x: Math.random() * width,
|
|
y: Math.random() * height,
|
|
radius: 100 + Math.random() * 150
|
|
});
|
|
}
|
|
|
|
// Create nodes with random positions and properties
|
|
for (let i = 0; i < this.config.nodeCount; i++) {
|
|
// Entscheide, ob dieser Knoten zu einem Cluster gehört oder nicht
|
|
const useCluster = Math.random() < this.config.clusteringFactor;
|
|
let x, y;
|
|
|
|
if (useCluster && clusters.length > 0) {
|
|
// Wähle ein zufälliges Cluster
|
|
const cluster = clusters[Math.floor(Math.random() * clusters.length)];
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const distance = Math.random() * cluster.radius;
|
|
|
|
// Platziere in der Nähe des Clusters mit einiger Streuung
|
|
x = cluster.x + Math.cos(angle) * distance;
|
|
y = cluster.y + Math.sin(angle) * distance;
|
|
|
|
// Stelle sicher, dass es innerhalb des Bildschirms bleibt
|
|
x = Math.max(0, Math.min(width, x));
|
|
y = Math.max(0, Math.min(height, y));
|
|
} else {
|
|
// Zufällige Position außerhalb von Clustern
|
|
x = Math.random() * width;
|
|
y = Math.random() * height;
|
|
}
|
|
|
|
// Bestimme die Knotengröße - wichtigere Knoten (in Clustern) sind größer
|
|
const nodeImportance = useCluster ? 1.2 : 0.8;
|
|
const size = this.config.nodeSize * nodeImportance + Math.random() * this.config.nodeVariation;
|
|
|
|
const node = {
|
|
x: x,
|
|
y: y,
|
|
size: size,
|
|
speed: {
|
|
x: (Math.random() - 0.5) * this.config.animationSpeed,
|
|
y: (Math.random() - 0.5) * this.config.animationSpeed
|
|
},
|
|
pulsePhase: Math.random() * Math.PI * 2, // Random starting phase
|
|
connections: [],
|
|
isActive: Math.random() < 0.3, // Some nodes start active for neural firing effect
|
|
lastFired: 0, // For neural firing animation
|
|
firingRate: 1000 + Math.random() * 4000 // Random firing rate in ms
|
|
};
|
|
|
|
this.nodes.push(node);
|
|
}
|
|
}
|
|
|
|
createConnections() {
|
|
this.connections = [];
|
|
this.flows = []; // Reset flows
|
|
|
|
// Create connections between nearby nodes
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const nodeA = this.nodes[i];
|
|
nodeA.connections = [];
|
|
|
|
// Sortiere andere Knoten nach Entfernung für bevorzugte nahe Verbindungen
|
|
const potentialConnections = [];
|
|
|
|
for (let j = 0; j < this.nodes.length; j++) {
|
|
if (i === j) continue;
|
|
|
|
const nodeB = this.nodes[j];
|
|
const dx = nodeB.x - nodeA.x;
|
|
const dy = nodeB.y - nodeA.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < this.config.connectionDistance) {
|
|
potentialConnections.push({
|
|
index: j,
|
|
distance: distance
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sortiere nach Entfernung
|
|
potentialConnections.sort((a, b) => a.distance - b.distance);
|
|
|
|
// Wähle die nächsten N Verbindungen, maximal maxConnections
|
|
const maxConn = Math.min(
|
|
this.config.maxConnections,
|
|
potentialConnections.length,
|
|
1 + Math.floor(Math.random() * this.config.maxConnections)
|
|
);
|
|
|
|
for (let c = 0; c < maxConn; c++) {
|
|
const connection = potentialConnections[c];
|
|
const j = connection.index;
|
|
const nodeB = this.nodes[j];
|
|
const distance = connection.distance;
|
|
|
|
// Create weighted connection (closer = stronger)
|
|
const connectionStrength = Math.max(0, 1 - distance / this.config.connectionDistance);
|
|
const connOpacity = connectionStrength * this.config.connectionOpacity;
|
|
|
|
// Check if connection already exists
|
|
if (!this.connections.some(conn =>
|
|
(conn.from === i && conn.to === j) || (conn.from === j && conn.to === i)
|
|
)) {
|
|
// Neue Verbindung startet mit progress=0 für animierten Aufbau
|
|
this.connections.push({
|
|
from: i,
|
|
to: j,
|
|
distance: distance,
|
|
opacity: connOpacity,
|
|
strength: connectionStrength,
|
|
hasFlow: false,
|
|
lastActivated: 0,
|
|
progress: 0 // Animationsfortschritt für Verbindungsaufbau
|
|
});
|
|
nodeA.connections.push(j);
|
|
nodeB.connections.push(i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
startAnimation() {
|
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
}
|
|
|
|
animate() {
|
|
// Update nodes
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
const now = Date.now();
|
|
|
|
// Simulate neural firing
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
|
|
// Check if node should fire based on its firing rate
|
|
if (now - node.lastFired > node.firingRate) {
|
|
node.isActive = true;
|
|
node.lastFired = now;
|
|
|
|
// 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)
|
|
);
|
|
|
|
if (conn) {
|
|
// Mark connection as recently activated
|
|
conn.lastActivated = now;
|
|
|
|
// Create a flow along this connection
|
|
if (Math.random() < conn.strength * 0.8) {
|
|
this.flows.push({
|
|
connection: conn,
|
|
progress: 0,
|
|
direction: conn.from === i, // Flow from activated node
|
|
length: this.config.flowLength + Math.random() * 0.1,
|
|
intensity: 0.7 + Math.random() * 0.3 // Random intensity for variation
|
|
});
|
|
}
|
|
|
|
// Probability for connected node to activate
|
|
if (Math.random() < conn.strength * 0.5) {
|
|
this.nodes[connIndex].isActive = true;
|
|
this.nodes[connIndex].lastFired = now - Math.random() * 500; // Slight variation
|
|
}
|
|
}
|
|
}
|
|
} else if (now - node.lastFired > 300) { // Deactivate after short period
|
|
node.isActive = false;
|
|
}
|
|
}
|
|
|
|
// Animierter Verbindungsaufbau: progress inkrementieren
|
|
for (const connection of this.connections) {
|
|
if (connection.progress < 1) {
|
|
// Langsamer Aufbau: Geschwindigkeit kann angepasst werden
|
|
connection.progress += 0.012; // Sehr langsam, für subtilen Effekt
|
|
if (connection.progress > 1) connection.progress = 1;
|
|
}
|
|
}
|
|
|
|
// Update flows
|
|
this.updateFlows();
|
|
|
|
// Occasionally create new flows along connections
|
|
if (Math.random() < this.config.flowDensity) {
|
|
this.createNewFlow();
|
|
}
|
|
|
|
// Recalculate connections occasionally for a living network
|
|
if (Math.random() < 0.01) { // Only recalculate 1% of the time for performance
|
|
this.createConnections();
|
|
}
|
|
|
|
// Render
|
|
if (this.useWebGL) {
|
|
this.renderWebGL();
|
|
} else {
|
|
this.renderCanvas();
|
|
}
|
|
|
|
// Continue animation
|
|
this.animationFrameId = requestAnimationFrame(this.animate.bind(this));
|
|
}
|
|
|
|
// New method to update flow animations
|
|
updateFlows() {
|
|
// Update existing flows
|
|
for (let i = this.flows.length - 1; i >= 0; i--) {
|
|
const flow = this.flows[i];
|
|
flow.progress += this.config.flowSpeed / flow.connection.distance;
|
|
|
|
// Remove completed flows
|
|
if (flow.progress > 1.0) {
|
|
this.flows.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
// Generate more flows for enhanced visibility
|
|
if (Math.random() < this.config.flowDensity * 2) {
|
|
this.createNewFlow();
|
|
}
|
|
}
|
|
|
|
// New method to create flow animations
|
|
createNewFlow() {
|
|
if (this.connections.length === 0) return;
|
|
|
|
// Select a random connection with preference for more connected nodes
|
|
let connectionIdx = Math.floor(Math.random() * this.connections.length);
|
|
let attempts = 0;
|
|
|
|
// Try to find a connection with more connected nodes
|
|
while (attempts < 5) {
|
|
const testIdx = Math.floor(Math.random() * this.connections.length);
|
|
const testConn = this.connections[testIdx];
|
|
const fromNode = this.nodes[testConn.from];
|
|
|
|
if (fromNode.connections.length > 2) {
|
|
connectionIdx = testIdx;
|
|
break;
|
|
}
|
|
attempts++;
|
|
}
|
|
|
|
const connection = this.connections[connectionIdx];
|
|
|
|
// Create a new flow along this connection
|
|
this.flows.push({
|
|
connection: connection,
|
|
progress: 0,
|
|
direction: Math.random() > 0.5, // Randomly decide direction
|
|
length: this.config.flowLength + Math.random() * 0.1 // Slightly vary lengths
|
|
});
|
|
}
|
|
|
|
renderWebGL() {
|
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
// Select shader program
|
|
this.gl.useProgram(this.programInfo.program);
|
|
|
|
// Set resolution uniform
|
|
this.gl.uniform2f(this.programInfo.uniformLocations.resolution, width, height);
|
|
|
|
// Draw connections first (behind nodes)
|
|
this.renderConnectionsWebGL();
|
|
|
|
// Draw flows on top of connections
|
|
this.renderFlowsWebGL();
|
|
|
|
// Draw nodes
|
|
this.renderNodesWebGL();
|
|
}
|
|
|
|
renderNodesWebGL() {
|
|
// Prepare node positions for WebGL
|
|
const positions = new Float32Array(this.nodes.length * 2);
|
|
const sizes = new Float32Array(this.nodes.length);
|
|
|
|
const now = Date.now();
|
|
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
positions[i * 2] = node.x;
|
|
positions[i * 2 + 1] = node.y;
|
|
|
|
// Sichtbarkeit der Neuronen erhöhen
|
|
let pulse = Math.sin(node.pulsePhase) * 0.22 + 1.08;
|
|
const connectivityFactor = 1 + (node.connections.length / this.config.maxConnections) * 0.8;
|
|
sizes[i] = node.size * pulse * connectivityFactor;
|
|
}
|
|
|
|
// Bind position buffer
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2, // components per vertex
|
|
this.gl.FLOAT, // data type
|
|
false, // normalize
|
|
0, // stride
|
|
0 // offset
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
|
|
// Bind size buffer
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, sizes, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.pointSize,
|
|
1, // components per vertex
|
|
this.gl.FLOAT, // data type
|
|
false, // normalize
|
|
0, // stride
|
|
0 // offset
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
// Enable blending for all nodes
|
|
this.gl.enable(this.gl.BLEND);
|
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE); // Additive blending for glow
|
|
|
|
// Draw each node individually with its own color
|
|
for (let i = 0; i < this.nodes.length; i++) {
|
|
const node = this.nodes[i];
|
|
|
|
// Set node color - more visible with active highlighting
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
const nodeColor = this.hexToRgb(colorObj.nodeColor);
|
|
const nodePulseColor = this.hexToRgb(colorObj.nodePulse);
|
|
|
|
// Use pulse color for active nodes
|
|
let r = nodeColor.r / 255;
|
|
let g = nodeColor.g / 255;
|
|
let b = nodeColor.b / 255;
|
|
|
|
// Active nodes get brighter color
|
|
if (node.isActive) {
|
|
r = (r + nodePulseColor.r / 255) / 2;
|
|
g = (g + nodePulseColor.g / 255) / 2;
|
|
b = (b + nodePulseColor.b / 255) / 2;
|
|
}
|
|
|
|
// Sehr sichtbare Knoten
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
r, g, b,
|
|
node.isActive ? 0.98 : 0.8 // Sehr sichtbar
|
|
);
|
|
|
|
// Draw each node individually for better control
|
|
this.gl.drawArrays(this.gl.POINTS, i, 1);
|
|
}
|
|
}
|
|
|
|
renderConnectionsWebGL() {
|
|
const now = Date.now();
|
|
for (const connection of this.connections) {
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
// Animierter Verbindungsaufbau: nur Teil der Linie zeichnen
|
|
const progress = connection.progress || 1;
|
|
const x1 = fromNode.x;
|
|
const y1 = fromNode.y;
|
|
const x2 = x1 + (toNode.x - x1) * progress;
|
|
const y2 = y1 + (toNode.y - y1) * progress;
|
|
const positions = new Float32Array([
|
|
x1, y1,
|
|
x2, y2
|
|
]);
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2,
|
|
this.gl.FLOAT,
|
|
false,
|
|
0,
|
|
0
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
let lineColor = this.hexToRgb(colorObj.connectionColor);
|
|
// Sehr dezente Grundopazität
|
|
let opacity = connection.opacity * 0.7;
|
|
if (now - connection.lastActivated < 800) {
|
|
lineColor = this.hexToRgb(colorObj.flowColor);
|
|
const timeFactor = 1 - ((now - connection.lastActivated) / 800);
|
|
opacity = Math.max(opacity, timeFactor * 0.32);
|
|
}
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
lineColor.r / 255,
|
|
lineColor.g / 255,
|
|
lineColor.b / 255,
|
|
opacity
|
|
);
|
|
// Sehr dünne Linien
|
|
this.gl.lineWidth(0.5);
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
}
|
|
|
|
// New method to render the flowing animations
|
|
renderFlowsWebGL() {
|
|
// For each flow, draw a segment along its connection
|
|
for (const flow of this.flows) {
|
|
const connection = flow.connection;
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
|
|
// Calculate flow position
|
|
const startProgress = flow.progress;
|
|
const endProgress = Math.min(1, startProgress + flow.length);
|
|
|
|
// If flow hasn't started yet or has finished
|
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
|
|
|
// Calculate actual positions
|
|
const direction = flow.direction ? 1 : -1;
|
|
let p1, p2;
|
|
|
|
if (direction > 0) {
|
|
p1 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * endProgress
|
|
};
|
|
} else {
|
|
p1 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
|
};
|
|
}
|
|
|
|
// Line positions for the flow
|
|
const positions = new Float32Array([
|
|
p1.x, p1.y,
|
|
p2.x, p2.y
|
|
]);
|
|
|
|
// Bind position buffer
|
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
|
|
this.gl.bufferData(this.gl.ARRAY_BUFFER, positions, this.gl.STATIC_DRAW);
|
|
this.gl.vertexAttribPointer(
|
|
this.programInfo.attribLocations.vertexPosition,
|
|
2, // components per vertex
|
|
this.gl.FLOAT, // data type
|
|
false, // normalize
|
|
0, // stride
|
|
0 // offset
|
|
);
|
|
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
|
|
|
|
// Disable point size attribute for lines
|
|
this.gl.disableVertexAttribArray(this.programInfo.attribLocations.pointSize);
|
|
|
|
// Fade the flow at the beginning and end
|
|
const fadeEdge = 0.2;
|
|
const fadeOpacity = Math.min(
|
|
startProgress / fadeEdge,
|
|
(1 - endProgress) / fadeEdge,
|
|
1
|
|
);
|
|
|
|
// Flow color - much stronger glow
|
|
const colorObj = this.isDarkMode ? this.darkModeColors : this.lightModeColors;
|
|
const flowColor = this.hexToRgb(colorObj.flowColor);
|
|
|
|
// Flows mit sanftem, aber sichtbarem Glow und höherer Opazität
|
|
this.gl.uniform4f(
|
|
this.programInfo.uniformLocations.color,
|
|
flowColor.r / 255,
|
|
flowColor.g / 255,
|
|
flowColor.b / 255,
|
|
0.55 * fadeOpacity * (flow.intensity || 1) // Dezenter, aber sichtbar
|
|
);
|
|
|
|
// Dünnere Flows für subtilen Effekt
|
|
this.gl.lineWidth(1.2);
|
|
this.gl.drawArrays(this.gl.LINES, 0, 2);
|
|
}
|
|
}
|
|
|
|
renderCanvas() {
|
|
// Clear canvas
|
|
const width = this.canvas.width / (window.devicePixelRatio || 1);
|
|
const height = this.canvas.height / (window.devicePixelRatio || 1);
|
|
|
|
this.ctx.clearRect(0, 0, width, height);
|
|
|
|
// Set background
|
|
const backgroundColor = this.isDarkMode
|
|
? this.darkModeColors.background
|
|
: this.lightModeColors.background;
|
|
|
|
this.ctx.fillStyle = backgroundColor;
|
|
this.ctx.fillRect(0, 0, width, height);
|
|
|
|
// Draw connections
|
|
const connectionColor = this.isDarkMode
|
|
? this.darkModeColors.connectionColor
|
|
: this.lightModeColors.connectionColor;
|
|
|
|
for (const connection of this.connections) {
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
// Animierter Verbindungsaufbau: nur Teil der Linie zeichnen
|
|
const progress = connection.progress || 1;
|
|
const x1 = fromNode.x;
|
|
const y1 = fromNode.y;
|
|
const x2 = x1 + (toNode.x - x1) * progress;
|
|
const y2 = y1 + (toNode.y - y1) * progress;
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(x1, y1);
|
|
this.ctx.lineTo(x2, y2);
|
|
const rgbColor = this.hexToRgb(connectionColor);
|
|
// Sehr dezente Opazität
|
|
this.ctx.strokeStyle = `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, ${connection.opacity * 0.7})`;
|
|
this.ctx.lineWidth = 0.5;
|
|
this.ctx.stroke();
|
|
}
|
|
|
|
// Draw flows
|
|
this.renderFlowsCanvas();
|
|
|
|
// Draw nodes
|
|
const nodeColor = this.isDarkMode
|
|
? this.darkModeColors.nodeColor
|
|
: this.lightModeColors.nodeColor;
|
|
|
|
const nodePulse = this.isDarkMode
|
|
? this.darkModeColors.nodePulse
|
|
: this.lightModeColors.nodePulse;
|
|
|
|
for (const node of this.nodes) {
|
|
// Sichtbarkeit der Neuronen erhöhen
|
|
const pulse = Math.sin(node.pulsePhase) * 0.18 + 1.08; // Leicht erhöhte Amplitude
|
|
const nodeSize = node.size * pulse * (node.connections.length > 3 ? 1.22 : 1);
|
|
const glow = this.ctx.createRadialGradient(
|
|
node.x, node.y, 0,
|
|
node.x, node.y, nodeSize * 2.2
|
|
);
|
|
glow.addColorStop(0, `rgba(${nodePulse.r}, ${nodePulse.g}, ${nodePulse.b}, 0.52)`);
|
|
glow.addColorStop(0.5, `rgba(${nodeColor.r}, ${nodeColor.g}, ${nodeColor.b}, 0.22)`);
|
|
glow.addColorStop(1, `rgba(${nodeColor.r}, ${nodeColor.g}, ${nodeColor.b}, 0)`);
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(node.x, node.y, nodeSize, 0, Math.PI * 2);
|
|
this.ctx.fillStyle = glow;
|
|
this.ctx.globalAlpha = node.isActive ? 0.95 : 0.7; // Sehr sichtbar
|
|
this.ctx.fill();
|
|
this.ctx.globalAlpha = 1.0;
|
|
}
|
|
}
|
|
|
|
// New method to render flows in Canvas mode
|
|
renderFlowsCanvas() {
|
|
if (!this.ctx) return;
|
|
|
|
const flowColor = this.isDarkMode
|
|
? this.darkModeColors.flowColor
|
|
: this.lightModeColors.flowColor;
|
|
|
|
const rgbFlowColor = this.hexToRgb(flowColor);
|
|
|
|
for (const flow of this.flows) {
|
|
const connection = flow.connection;
|
|
const fromNode = this.nodes[connection.from];
|
|
const toNode = this.nodes[connection.to];
|
|
|
|
// Calculate flow position
|
|
const startProgress = flow.progress;
|
|
const endProgress = Math.min(1, startProgress + flow.length);
|
|
|
|
// If flow hasn't started yet or has finished
|
|
if (startProgress >= 1 || endProgress <= 0) continue;
|
|
|
|
// Calculate actual positions
|
|
const direction = flow.direction ? 1 : -1;
|
|
let p1, p2;
|
|
|
|
if (direction > 0) {
|
|
p1 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * startProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: fromNode.x + (toNode.x - fromNode.x) * endProgress,
|
|
y: fromNode.y + (toNode.y - fromNode.y) * startProgress
|
|
};
|
|
} else {
|
|
p1 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * startProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * startProgress
|
|
};
|
|
p2 = {
|
|
x: toNode.x + (fromNode.x - toNode.x) * endProgress,
|
|
y: toNode.y + (fromNode.y - toNode.y) * endProgress
|
|
};
|
|
}
|
|
|
|
// Fade the flow at the beginning and end
|
|
const fadeEdge = 0.2;
|
|
const fadeOpacity = Math.min(
|
|
startProgress / fadeEdge,
|
|
(1 - endProgress) / fadeEdge,
|
|
1
|
|
);
|
|
|
|
// Dezente Flows mit sanftem Fade-Out
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(p1.x, p1.y);
|
|
this.ctx.lineTo(p2.x, p2.y);
|
|
this.ctx.strokeStyle = `rgba(${rgbFlowColor.r}, ${rgbFlowColor.g}, ${rgbFlowColor.b}, ${0.28 * fadeOpacity})`;
|
|
this.ctx.lineWidth = 1.1;
|
|
this.ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// Helper method to convert hex to RGB
|
|
hexToRgb(hex) {
|
|
// Remove # if present
|
|
hex = hex.replace(/^#/, '');
|
|
|
|
// Handle rgba hex format
|
|
let alpha = 1;
|
|
if (hex.length === 8) {
|
|
alpha = parseInt(hex.slice(6, 8), 16) / 255;
|
|
hex = hex.slice(0, 6);
|
|
}
|
|
|
|
// Parse hex values
|
|
const bigint = parseInt(hex, 16);
|
|
const r = (bigint >> 16) & 255;
|
|
const g = (bigint >> 8) & 255;
|
|
const b = bigint & 255;
|
|
|
|
return { r, g, b, a: alpha };
|
|
}
|
|
|
|
// Cleanup method
|
|
destroy() {
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
}
|
|
|
|
window.removeEventListener('resize', this.resizeCanvas.bind(this));
|
|
|
|
if (this.canvas && this.canvas.parentNode) {
|
|
this.canvas.parentNode.removeChild(this.canvas);
|
|
}
|
|
|
|
if (this.gl) {
|
|
// Clean up WebGL resources
|
|
this.gl.deleteBuffer(this.positionBuffer);
|
|
this.gl.deleteBuffer(this.sizeBuffer);
|
|
this.gl.deleteProgram(this.shaderProgram);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Short delay to ensure DOM is fully loaded
|
|
setTimeout(() => {
|
|
if (!window.neuralNetworkBackground) {
|
|
console.log('Creating Neural Network Background');
|
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
// Re-initialize when page is fully loaded (for safety)
|
|
window.addEventListener('load', () => {
|
|
if (!window.neuralNetworkBackground) {
|
|
console.log('Re-initializing Neural Network Background on full load');
|
|
window.neuralNetworkBackground = new NeuralNetworkBackground();
|
|
}
|
|
});
|
|
|
|
// Clean up when window is closed
|
|
window.addEventListener('beforeunload', () => {
|
|
if (window.neuralNetworkBackground) {
|
|
window.neuralNetworkBackground.destroy();
|
|
}
|
|
});
|