Custom Code Experiment •
Music: Ariomaniacs Part 2
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Music: Ariomaniacs Part 2</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@200;300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@2.44.0/tabler-icons.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
body {
overflow: hidden;
background: #050508;
font-family: 'Outfit', sans-serif;
}
canvas {
display: block;
width: 100%;
height: 100%;
cursor: crosshair;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
/* MORPH BUTTON — sits in top-right, subtle, out of the way */
#morph-container {
position: absolute;
top: 24px;
right: 28px;
z-index: 20;
}
.morph-button {
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
background: rgba(20, 20, 40, 0.25);
border: 1px solid rgba(255, 255, 255, 0.14);
color: rgba(255, 255, 255, 0.55);
padding: 8px 20px;
border-radius: 60px;
cursor: pointer;
font-family: 'Outfit', sans-serif;
font-size: 11px;
font-weight: 400;
letter-spacing: 2.5px;
text-transform: uppercase;
transition: all 0.3s ease;
box-shadow: none;
overflow: hidden;
position: relative;
}
.morph-button span {
position: relative;
z-index: 2;
display: inline-block;
}
.morph-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transform: skewX(-15deg);
transition: 0.5s;
pointer-events: none;
}
.morph-button:hover {
background: rgba(80, 90, 160, 0.35);
border-color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.9);
letter-spacing: 3px;
}
.morph-button:hover::before {
left: 150%;
}
.morph-button:active {
transform: scale(0.97);
}
/* AUDIO PLAYER */
#audio-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 10px 0 14px;
background: linear-gradient(to top, rgba(5,5,12,0.92) 0%, transparent 100%);
}
#track-info {
text-align: center;
}
#track-title {
font-size: 13px;
font-weight: 400;
color: rgba(180, 160, 255, 0.85);
letter-spacing: 0.1em;
font-family: 'Outfit', sans-serif;
text-transform: uppercase;
}
#track-artist {
font-size: 10px;
color: rgba(180, 160, 255, 0.35);
margin-top: 2px;
font-family: 'Outfit', sans-serif;
letter-spacing: 0.08em;
}
#progress-wrap {
width: 88%;
max-width: 580px;
display: flex;
align-items: center;
gap: 10px;
}
#time-cur, #time-dur {
font-size: 10px;
color: rgba(180, 160, 255, 0.35);
min-width: 34px;
font-variant-numeric: tabular-nums;
font-family: 'Outfit', sans-serif;
}
#time-dur { text-align: right; }
#progress-bar {
flex: 1;
height: 2px;
background: rgba(150, 120, 255, 0.15);
border-radius: 2px;
position: relative;
cursor: pointer;
}
#progress-fill {
height: 100%;
width: 0%;
border-radius: 2px;
background: rgba(150, 120, 255, 0.6);
pointer-events: none;
transition: width 0.1s linear;
}
#progress-thumb {
position: absolute;
right: -5px;
top: 50%;
transform: translateY(-50%);
width: 9px;
height: 9px;
border-radius: 50%;
background: rgba(180, 160, 255, 0.9);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}
#progress-bar:hover #progress-thumb { opacity: 1; }
#btn-row {
display: flex;
align-items: center;
gap: 18px;
}
#play-btn {
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(150, 120, 255, 0.35);
background: rgba(80, 60, 140, 0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: rgba(180, 160, 255, 0.9);
transition: transform 0.12s, background 0.15s, box-shadow 0.2s;
flex-shrink: 0;
}
#play-btn:hover {
background: rgba(100, 80, 180, 0.3);
transform: scale(1.06);
box-shadow: 0 0 14px rgba(130, 100, 255, 0.4);
}
#play-btn:active { transform: scale(0.95); }
#vol-wrap {
display: flex;
align-items: center;
gap: 8px;
}
#vol-icon {
font-size: 14px;
color: rgba(180, 160, 255, 0.35);
}
#vol-slider {
-webkit-appearance: none;
appearance: none;
width: 68px;
height: 2px;
background: rgba(150, 120, 255, 0.2);
border-radius: 2px;
outline: none;
cursor: pointer;
}
#vol-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(180, 160, 255, 0.85);
cursor: pointer;
}
#vol-slider::-moz-range-thumb {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(180, 160, 255, 0.85);
cursor: pointer;
border: none;
}
/* Audio reactive indicator ring on play button */
#play-btn.active-audio {
box-shadow: 0 0 0 2px rgba(150, 120, 255, 0.25), 0 0 18px rgba(130, 100, 255, 0.5);
}
@media (max-width: 640px) {
.morph-button {
padding: 10px 28px;
font-size: 15px;
letter-spacing: 1.5px;
}
#morph-container {
bottom: 95px;
}
}
</style>
</head>
<body>
<canvas id="neural-network-canvas"></canvas>
<div id="morph-container">
<button id="change-formation-btn" class="morph-button"><span>MORPH</span></button>
</div>
<!-- AUDIO PLAYER -->
<div id="audio-controls">
<div id="track-info">
<div id="track-title">Ariomaniacs Part 2</div>
<div id="track-artist">Mike H</div>
</div>
<div id="progress-wrap">
<span id="time-cur">0:00</span>
<div id="progress-bar"><div id="progress-fill"></div><div id="progress-thumb"></div></div>
<span id="time-dur">0:00</span>
</div>
<div id="btn-row">
<div id="vol-wrap">
<i class="ti ti-volume" id="vol-icon"></i>
<input type="range" id="vol-slider" min="0" max="1" step="0.01" value="0.8">
</div>
<button id="play-btn" aria-label="Play"><i class="ti ti-player-play" id="play-icon"></i></button>
<div style="width:88px"></div>
</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.162.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.162.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
// =====================================================================
// AUDIO ENGINE
// =====================================================================
const AUDIO_URL = 'https://cdn.jsdelivr.net/gh/alaric-visi/music@main/ariomaniacs/02-ariomaniacs-part-2.mp3';
const playBtn = document.getElementById('play-btn');
const playIcon = document.getElementById('play-icon');
const progressBar = document.getElementById('progress-bar');
const progressFill= document.getElementById('progress-fill');
const timeCur = document.getElementById('time-cur');
const timeDur = document.getElementById('time-dur');
const volSlider = document.getElementById('vol-slider');
const volIcon = document.getElementById('vol-icon');
let audioCtx, analyser, audioSource, gainNode;
let audioBuffer = null, isPlaying = false, startTime = 0, pauseOffset = 0, audioDuration = 0;
let freqData, waveData;
let audioInitialized = false;
// Smoothed audio bands
let sBass = 0, sMid = 0, sHigh = 0, sEnergy = 0;
function fmtTime(t) {
if (!isFinite(t) || t < 0) t = 0;
return `${Math.floor(t/60)}:${Math.floor(t%60).toString().padStart(2,'0')}`;
}
async function initAudio() {
if (audioInitialized) return;
audioInitialized = true;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioCtx.createGain();
gainNode.gain.value = parseFloat(volSlider.value);
gainNode.connect(audioCtx.destination);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.80;
analyser.connect(gainNode);
freqData = new Uint8Array(analyser.frequencyBinCount);
waveData = new Uint8Array(analyser.fftSize);
try {
const res = await fetch(AUDIO_URL);
const ab = await res.arrayBuffer();
audioBuffer = await audioCtx.decodeAudioData(ab);
audioDuration = audioBuffer.duration;
timeDur.textContent = fmtTime(audioDuration);
playAudio();
} catch(e) {
audioInitialized = false;
console.warn('Audio load failed:', e);
}
}
function playAudio() {
if (!audioBuffer) return;
if (audioCtx.state === 'suspended') audioCtx.resume();
audioSource = audioCtx.createBufferSource();
audioSource.buffer = audioBuffer;
audioSource.connect(analyser);
audioSource.start(0, pauseOffset);
startTime = audioCtx.currentTime - pauseOffset;
isPlaying = true;
playIcon.className = 'ti ti-player-pause';
playBtn.classList.add('active-audio');
audioSource.onended = () => {
if (isPlaying) {
pauseOffset = 0; isPlaying = false;
playIcon.className = 'ti ti-player-play';
playBtn.classList.remove('active-audio');
}
};
}
function pauseAudio() {
if (audioSource) { audioSource.onended = null; audioSource.stop(); }
pauseOffset = Math.max(0, audioCtx.currentTime - startTime);
isPlaying = false;
playIcon.className = 'ti ti-player-play';
playBtn.classList.remove('active-audio');
}
playBtn.addEventListener('click', async () => {
if (!audioInitialized) { await initAudio(); return; }
if (!audioBuffer) return;
isPlaying ? pauseAudio() : playAudio();
});
volSlider.addEventListener('input', () => {
if (gainNode) gainNode.gain.value = parseFloat(volSlider.value);
volIcon.className = parseFloat(volSlider.value) < 0.01 ? 'ti ti-volume-off' : 'ti ti-volume';
});
progressBar.addEventListener('click', (e) => {
if (!audioBuffer) return;
const rect = progressBar.getBoundingClientRect();
const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
pauseOffset = frac * audioDuration;
if (isPlaying) { if (audioSource) { audioSource.onended=null; audioSource.stop(); } playAudio(); }
else { timeCur.textContent=fmtTime(pauseOffset); progressFill.style.width=(frac*100)+'%'; }
});
// Returns smoothed audio bands {bass, mid, high, energy}
// Falls back to gentle idle animation when not playing
function getAudio(elapsedTime) {
let energy=0, bass=0, mid=0, high=0;
if (analyser && isPlaying) {
analyser.getByteFrequencyData(freqData);
for (let i=0; i<freqData.length; i++) energy += freqData[i];
energy /= freqData.length * 255;
for (let i=0; i<8; i++) bass += freqData[i]; bass /= 8*255;
for (let i=8; i<80; i++) mid += freqData[i]; mid /= 72*255;
for (let i=80; i<200; i++) high += freqData[i]; high /= 120*255;
} else {
const idle = (Math.sin(elapsedTime * 0.8) * 0.5 + 0.5) * 0.03;
energy=idle; bass=idle*0.5; mid=idle*0.3; high=idle*0.1;
}
const rise = 0.45, fall = 0.14;
sBass += (bass - sBass) * (bass > sBass ? rise : fall);
sMid += (mid - sMid) * (mid > sMid ? rise : fall);
sHigh += (high - sHigh) * (high > sHigh ? rise : fall);
sEnergy += (energy - sEnergy) * (energy > sEnergy ? rise : fall);
return { bass: sBass, mid: sMid, high: sHigh, energy: sEnergy };
}
// =====================================================================
// CONFIG
// =====================================================================
const config = {
paused: false,
activePaletteIndex: 0,
currentFormation: 0,
numFormations: 3,
densityFactor: 1.0
};
const colorPalettes = [
[
new THREE.Color(0x667eea),
new THREE.Color(0x764ba2),
new THREE.Color(0xf093fb),
new THREE.Color(0x9d50bb),
new THREE.Color(0x6e48aa)
],
[
new THREE.Color(0xf857a6),
new THREE.Color(0xff5858),
new THREE.Color(0xfeca57),
new THREE.Color(0xff6348),
new THREE.Color(0xff9068)
],
[
new THREE.Color(0x4facfe),
new THREE.Color(0x00f2fe),
new THREE.Color(0x43e97b),
new THREE.Color(0x38f9d7),
new THREE.Color(0x4484ce)
]
];
// =====================================================================
// THREE.JS SETUP
// =====================================================================
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.002);
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 8, 28);
const canvasElement = document.getElementById('neural-network-canvas');
const renderer = new THREE.WebGLRenderer({
canvas: canvasElement,
antialias: true,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x000000);
renderer.outputColorSpace = THREE.SRGBColorSpace;
// Starfield
function createStarfield() {
const count = 8000;
const positions = [], colors = [], sizes = [];
for (let i = 0; i < count; i++) {
const r = THREE.MathUtils.randFloat(50, 150);
const phi = Math.acos(THREE.MathUtils.randFloatSpread(2));
const theta = THREE.MathUtils.randFloat(0, Math.PI * 2);
positions.push(r*Math.sin(phi)*Math.cos(theta), r*Math.sin(phi)*Math.sin(theta), r*Math.cos(phi));
const c = Math.random();
if (c < 0.7) colors.push(1, 1, 1);
else if (c < 0.85) colors.push(0.7, 0.8, 1);
else colors.push(1, 0.9, 0.8);
sizes.push(THREE.MathUtils.randFloat(0.1, 0.3));
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geo.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
const mat = new THREE.ShaderMaterial({
uniforms: { uTime: { value: 0 }, uHighFreq: { value: 0 } },
vertexShader: `
attribute float size;
attribute vec3 color;
varying vec3 vColor;
uniform float uTime;
uniform float uHighFreq;
void main() {
vColor = color;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
// High-frequency audio makes stars twinkle faster & brighter
float twinkleSpeed = 2.0 + uHighFreq * 12.0;
float twinkleAmp = 0.3 + uHighFreq * 0.7;
float twinkle = sin(uTime * twinkleSpeed + position.x * 100.0) * twinkleAmp + (1.0 - twinkleAmp * 0.5);
gl_PointSize = size * twinkle * (1.0 + uHighFreq * 1.5) * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
vec2 center = gl_PointCoord - 0.5;
float dist = length(center);
if (dist > 0.5) discard;
float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
gl_FragColor = vec4(vColor, alpha * 0.8);
}
`,
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending
});
return new THREE.Points(geo, mat);
}
const starField = createStarfield();
scene.add(starField);
// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.rotateSpeed = 0.7;
controls.minDistance = 8;
controls.maxDistance = 80;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.22;
controls.enablePan = false;
// Post-processing
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.8, 0.6, 0.7
);
composer.addPass(bloomPass);
composer.addPass(new OutputPass());
// =====================================================================
// SHADER UNIFORMS
// =====================================================================
const pulseUniforms = {
uTime: { value: 0.0 },
uPulsePositions: { value: [new THREE.Vector3(1e3,1e3,1e3), new THREE.Vector3(1e3,1e3,1e3), new THREE.Vector3(1e3,1e3,1e3)] },
uPulseTimes: { value: [-1e3, -1e3, -1e3] },
uPulseColors: { value: [new THREE.Color(1,1,1), new THREE.Color(1,1,1), new THREE.Color(1,1,1)] },
uPulseSpeed: { value: 18.0 },
uBaseNodeSize: { value: 0.6 },
// Audio uniforms — each band is a separate value so the shader
// can pick the right one per-node based on the freqBand attribute
uBass: { value: 0.0 },
uMid: { value: 0.0 },
uHigh: { value: 0.0 },
uEnergy: { value: 0.0 },
};
// =====================================================================
// NOISE & SHADERS
// =====================================================================
const noiseFunctions = `
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
const vec2 C = vec2(1.0/6.0, 1.0/3.0);
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
vec3 i = floor(v + dot(v, C.yyy));
vec3 x0 = v - i + dot(i, C.xxx);
vec3 g = step(x0.yzx, x0.xyz);
vec3 l = 1.0 - g;
vec3 i1 = min(g.xyz, l.zxy);
vec3 i2 = max(g.xyz, l.zxy);
vec3 x1 = x0 - i1 + C.xxx;
vec3 x2 = x0 - i2 + C.yyy;
vec3 x3 = x0 - D.yyy;
i = mod289(i);
vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
float n_ = 0.142857142857;
vec3 ns = n_ * D.wyz - D.xzx;
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
vec4 x_ = floor(j * ns.z);
vec4 y_ = floor(j - 7.0 * x_);
vec4 x = x_ * ns.x + ns.yyyy;
vec4 y = y_ * ns.x + ns.yyyy;
vec4 h = 1.0 - abs(x) - abs(y);
vec4 b0 = vec4(x.xy, y.xy);
vec4 b1 = vec4(x.zw, y.zw);
vec4 s0 = floor(b0) * 2.0 + 1.0;
vec4 s1 = floor(b1) * 2.0 + 1.0;
vec4 sh = -step(h, vec4(0.0));
vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
vec3 p0 = vec3(a0.xy, h.x);
vec3 p1 = vec3(a0.zw, h.y);
vec3 p2 = vec3(a1.xy, h.z);
vec3 p3 = vec3(a1.zw, h.w);
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
m = m * m;
return 42.0 * dot(m * m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
}`;
const nodeShader = {
vertexShader: `${noiseFunctions}
attribute float nodeSize;
attribute float nodeType;
attribute vec3 nodeColor;
attribute float distanceFromRoot;
// freqBand: 0 = bass, 1 = mid, 2 = high — assigned per-node at build time
attribute float freqBand;
uniform float uTime;
uniform vec3 uPulsePositions[3];
uniform float uPulseTimes[3];
uniform float uPulseSpeed;
uniform float uBaseNodeSize;
uniform float uBass;
uniform float uMid;
uniform float uHigh;
uniform float uEnergy;
varying vec3 vColor;
varying float vNodeType;
varying vec3 vPosition;
varying float vPulseIntensity;
varying float vDistanceFromRoot;
varying float vGlow;
// Pass per-node audio reactivity to fragment shader
varying float vNodeAudio;
float getPulseIntensity(vec3 worldPos, vec3 pulsePos, float pulseTime) {
if (pulseTime < 0.0) return 0.0;
float timeSinceClick = uTime - pulseTime;
if (timeSinceClick < 0.0 || timeSinceClick > 4.0) return 0.0;
float pulseRadius = timeSinceClick * uPulseSpeed;
float distToClick = distance(worldPos, pulsePos);
float waveProximity = abs(distToClick - pulseRadius);
return smoothstep(3.0, 0.0, waveProximity) * smoothstep(4.0, 0.0, timeSinceClick);
}
void main() {
vNodeType = nodeType;
vColor = nodeColor;
vDistanceFromRoot = distanceFromRoot;
vec3 worldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vPosition = worldPos;
float totalPulseIntensity = 0.0;
for (int i = 0; i < 3; i++) {
totalPulseIntensity += getPulseIntensity(worldPos, uPulsePositions[i], uPulseTimes[i]);
}
vPulseIntensity = min(totalPulseIntensity, 1.0);
// Each node has a PRIMARY band it reacts to strongly,
// plus BLEED from the other bands so it's never completely silent.
// freqBand: 0=bass, 1=mid, 2=high
float primary = freqBand < 0.5 ? uBass :
freqBand < 1.5 ? uMid : uHigh;
float secondary = freqBand < 0.5 ? uMid :
freqBand < 1.5 ? uBass : uMid;
float tertiary = freqBand < 0.5 ? uHigh :
freqBand < 1.5 ? uHigh : uBass;
// Noise floor: every node always has at least a little movement
float noiseFloor = 0.04;
float myAudio = max(noiseFloor, primary * 0.70 + secondary * 0.20 + tertiary * 0.10);
vNodeAudio = myAudio;
// Size breathe — primary band drives it, with real amplitude
float breathe = sin(uTime * 0.7 + distanceFromRoot * 0.15) * 0.12 + 0.88;
float audioSwell = myAudio * 1.4;
float baseSize = nodeSize * (breathe + audioSwell);
float pulseSize = baseSize * (1.0 + vPulseIntensity * 2.0);
vGlow = 0.4 + 0.4 * sin(uTime * 0.5 + distanceFromRoot * 0.2) + myAudio * 0.5;
vec3 modifiedPosition = position;
if (nodeType > 0.5) {
// Leaf nodes ripple with their own band, not global bass
float noiseSpeed = 0.08 + myAudio * 0.18;
float noiseAmp = 0.12 + myAudio * 0.28; // was 0.55 — calmer
float noise = snoise(position * 0.08 + uTime * noiseSpeed);
modifiedPosition += normal * noise * noiseAmp;
}
vec4 mvPosition = modelViewMatrix * vec4(modifiedPosition, 1.0);
gl_PointSize = pulseSize * uBaseNodeSize * (1000.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
}`,
fragmentShader: `
uniform float uTime;
uniform vec3 uPulseColors[3];
uniform float uEnergy;
varying vec3 vColor;
varying float vNodeType;
varying vec3 vPosition;
varying float vPulseIntensity;
varying float vDistanceFromRoot;
varying float vGlow;
varying float vNodeAudio;
void main() {
vec2 center = 2.0 * gl_PointCoord - 1.0;
float dist = length(center);
if (dist > 1.0) discard;
float glow1 = 1.0 - smoothstep(0.0, 0.5, dist);
float glow2 = 1.0 - smoothstep(0.0, 1.0, dist);
float glowStrength = pow(glow1, 1.2) + glow2 * 0.25;
float breatheColor = 0.92 + 0.08 * sin(uTime * 0.6 + vDistanceFromRoot * 0.25);
vec3 baseColor = vColor * breatheColor;
// Colour lifts toward a brighter version of its own hue on its band —
// capped so it never blows to white, but clearly visible
baseColor = mix(baseColor, baseColor * 1.5, vNodeAudio * 0.8);
vec3 finalColor = baseColor;
if (vPulseIntensity > 0.0) {
vec3 pulseColor = mix(vec3(1.0), uPulseColors[0], 0.5);
finalColor = mix(baseColor, pulseColor, vPulseIntensity * 0.32);
finalColor *= (1.0 + vPulseIntensity * 0.35);
glowStrength *= (1.0 + vPulseIntensity * 0.4);
}
// Core highlight scales with audio
float coreBrightness = smoothstep(0.4, 0.0, dist);
finalColor += vec3(1.0) * coreBrightness * (0.18 + vNodeAudio * 0.28);
float alpha = glowStrength * (0.92 - 0.28 * dist);
float camDistance = length(vPosition - cameraPosition);
float distanceFade = smoothstep(100.0, 15.0, camDistance);
finalColor *= (1.0 + vGlow * 0.08);
gl_FragColor = vec4(finalColor, alpha * distanceFade);
}`
};
const connectionShader = {
vertexShader: `${noiseFunctions}
attribute vec3 startPoint;
attribute vec3 endPoint;
attribute float connectionStrength;
attribute float pathIndex;
attribute vec3 connectionColor;
// freqBand for this connection (averaged from its two nodes)
attribute float freqBand;
uniform float uTime;
uniform vec3 uPulsePositions[3];
uniform float uPulseTimes[3];
uniform float uPulseSpeed;
uniform float uBass;
uniform float uMid;
uniform float uHigh;
varying vec3 vColor;
varying float vConnectionStrength;
varying float vPulseIntensity;
varying float vPathPosition;
varying float vDistanceFromCamera;
varying float vConnAudio;
float getPulseIntensity(vec3 worldPos, vec3 pulsePos, float pulseTime) {
if (pulseTime < 0.0) return 0.0;
float timeSinceClick = uTime - pulseTime;
if (timeSinceClick < 0.0 || timeSinceClick > 4.0) return 0.0;
float pulseRadius = timeSinceClick * uPulseSpeed;
float distToClick = distance(worldPos, pulsePos);
float waveProximity = abs(distToClick - pulseRadius);
return smoothstep(3.0, 0.0, waveProximity) * smoothstep(4.0, 0.0, timeSinceClick);
}
void main() {
float t = position.x;
vPathPosition = t;
vec3 midPoint = mix(startPoint, endPoint, 0.5);
float pathOffset = sin(t * 3.14159) * 0.15;
vec3 perpendicular = normalize(cross(normalize(endPoint - startPoint), vec3(0.0, 1.0, 0.0)));
if (length(perpendicular) < 0.1) perpendicular = vec3(1.0, 0.0, 0.0);
midPoint += perpendicular * pathOffset;
vec3 p0 = mix(startPoint, midPoint, t);
vec3 p1 = mix(midPoint, endPoint, t);
vec3 finalPos = mix(p0, p1, t);
// Connection listens primarily to its own band, with bleed from others
float primary = freqBand < 0.5 ? uBass :
freqBand < 1.5 ? uMid : uHigh;
float secondary = freqBand < 0.5 ? uMid :
freqBand < 1.5 ? uBass : uMid;
float noiseFloor = 0.04;
float myAudio = max(noiseFloor, primary * 0.75 + secondary * 0.25);
vConnAudio = myAudio;
// Path ripple driven by this connection's own audio band
float noiseTime = uTime * (0.12 + myAudio * 0.35);
float noiseAmp = 0.10 + myAudio * 0.30;
float noise = snoise(vec3(pathIndex * 0.08, t * 0.6, noiseTime));
finalPos += perpendicular * noise * noiseAmp;
vec3 worldPos = (modelMatrix * vec4(finalPos, 1.0)).xyz;
float totalPulseIntensity = 0.0;
for (int i = 0; i < 3; i++) totalPulseIntensity += getPulseIntensity(worldPos, uPulsePositions[i], uPulseTimes[i]);
vPulseIntensity = min(totalPulseIntensity, 1.0);
vColor = connectionColor;
vConnectionStrength = connectionStrength;
vDistanceFromCamera = length(worldPos - cameraPosition);
gl_Position = projectionMatrix * modelViewMatrix * vec4(finalPos, 1.0);
}`,
fragmentShader: `
uniform float uTime;
uniform vec3 uPulseColors[3];
varying vec3 vColor;
varying float vConnectionStrength;
varying float vPulseIntensity;
varying float vPathPosition;
varying float vDistanceFromCamera;
varying float vConnAudio;
void main() {
// Flow speed scales with this connection's own band — clearly visible
float flowSpeed1 = 3.5 + vConnAudio * 16.0;
float flowSpeed2 = 2.0 + vConnAudio * 9.0;
float flowPattern1 = sin(vPathPosition * 25.0 - uTime * flowSpeed1) * 0.5 + 0.5;
float flowPattern2 = sin(vPathPosition * 15.0 - uTime * flowSpeed2 + 1.57) * 0.5 + 0.5;
float combinedFlow = (flowPattern1 + flowPattern2 * 0.5) / 1.5;
vec3 baseColor = vColor * (0.82 + 0.18 * sin(uTime * 0.6 + vPathPosition * 12.0));
// Visible colour lift on beat, capped to avoid blowout
baseColor = mix(baseColor, baseColor * 1.45, vConnAudio * 0.75);
float flowIntensity = 0.35 * combinedFlow * vConnectionStrength + vConnAudio * 0.35;
vec3 finalColor = baseColor;
if (vPulseIntensity > 0.0) {
vec3 pulseColor = mix(vec3(1.0), uPulseColors[0], 0.4);
finalColor = mix(baseColor, pulseColor * 1.1, vPulseIntensity * 0.27);
flowIntensity += vPulseIntensity * 0.25;
}
finalColor *= (0.72 + flowIntensity + vConnectionStrength * 0.4);
float baseAlpha = (0.65 + vConnAudio * 0.3) * vConnectionStrength;
float flowAlpha = combinedFlow * (0.25 + vConnAudio * 0.2);
float alpha = baseAlpha + flowAlpha;
alpha = mix(alpha, min(1.0, alpha * 2.0), vPulseIntensity);
float distanceFade = smoothstep(100.0, 15.0, vDistanceFromCamera);
gl_FragColor = vec4(finalColor, alpha * distanceFade);
}`
};
// =====================================================================
// NETWORK GENERATION (unchanged logic)
// =====================================================================
class Node {
constructor(position, level = 0, type = 0) {
this.position = position;
this.connections = [];
this.level = level;
this.type = type;
this.size = type === 0 ? THREE.MathUtils.randFloat(0.8, 1.4) : THREE.MathUtils.randFloat(0.5, 1.0);
this.distanceFromRoot = 0;
}
addConnection(node, strength = 1.0) {
if (!this.isConnectedTo(node)) {
this.connections.push({ node, strength });
node.connections.push({ node: this, strength });
}
}
isConnectedTo(node) { return this.connections.some(c => c.node === node); }
}
function generateNeuralNetwork(formationIndex, densityFactor = 1.0) {
let nodes = [], rootNode;
const palette = colorPalettes[config.activePaletteIndex];
function generateCrystallineSphere() {
rootNode = new Node(new THREE.Vector3(0,0,0), 0, 0); rootNode.size = 2.0; nodes.push(rootNode);
const layers = 5, goldenRatio = (1 + Math.sqrt(5)) / 2;
for (let layer = 1; layer <= layers; layer++) {
const radius = layer * 4;
const numPoints = Math.floor(layer * 12 * densityFactor);
for (let i = 0; i < numPoints; i++) {
const phi = Math.acos(1 - 2*(i+0.5)/numPoints);
const theta = 2*Math.PI*i/goldenRatio;
const pos = new THREE.Vector3(radius*Math.sin(phi)*Math.cos(theta), radius*Math.sin(phi)*Math.sin(theta), radius*Math.cos(phi));
const isLeaf = layer === layers || Math.random() < 0.3;
const node = new Node(pos, layer, isLeaf ? 1 : 0);
node.distanceFromRoot = radius; nodes.push(node);
if (layer > 1) {
const prev = nodes.filter(n => n.level === layer-1 && n !== rootNode);
prev.sort((a,b) => pos.distanceTo(a.position) - pos.distanceTo(b.position));
for (let j = 0; j < Math.min(3, prev.length); j++) {
const dist = pos.distanceTo(prev[j].position);
node.addConnection(prev[j], Math.max(0.3, 1.0-(dist/(radius*2))));
}
} else { rootNode.addConnection(node, 0.9); }
}
const layerNodes = nodes.filter(n => n.level === layer && n !== rootNode);
for (let i = 0; i < layerNodes.length; i++) {
const nearby = layerNodes.filter(n => n !== layerNodes[i]).sort((a,b) => layerNodes[i].position.distanceTo(a.position)-layerNodes[i].position.distanceTo(b.position)).slice(0,5);
for (const nn of nearby) {
if (layerNodes[i].position.distanceTo(nn.position) < radius*0.8 && !layerNodes[i].isConnectedTo(nn))
layerNodes[i].addConnection(nn, 0.6);
}
}
}
}
function generateHelixLattice() {
rootNode = new Node(new THREE.Vector3(0,0,0), 0, 0); rootNode.size = 1.8; nodes.push(rootNode);
const numHelices=4, height=30, maxRadius=12, npH=Math.floor(50*densityFactor);
let helixArrays=[];
for (let h=0; h<numHelices; h++) {
const hPhase=(h/numHelices)*Math.PI*2; let hNodes=[];
for (let i=0; i<npH; i++) {
const t=i/(npH-1), y=(t-0.5)*height, rScale=Math.sin(t*Math.PI)*0.7+0.3, r=maxRadius*rScale, angle=hPhase+t*Math.PI*6;
const pos=new THREE.Vector3(r*Math.cos(angle),y,r*Math.sin(angle));
const node=new Node(pos,Math.ceil(t*5),(i>npH-5||Math.random()<0.25)?1:0);
node.distanceFromRoot=Math.sqrt(r*r+y*y); node.helixT=t; node.helixIndex=h;
nodes.push(node); hNodes.push(node);
}
helixArrays.push(hNodes); rootNode.addConnection(hNodes[0],1.0);
for (let i=0;i<hNodes.length-1;i++) hNodes[i].addConnection(hNodes[i+1],0.85);
}
for (let h=0;h<numHelices;h++) {
const cur=helixArrays[h], nxt=helixArrays[(h+1)%numHelices];
for (let i=0;i<cur.length;i+=5) { const t=cur[i].helixT, idx=Math.round(t*(nxt.length-1)); if(idx<nxt.length) cur[i].addConnection(nxt[idx],0.7); }
}
}
function generateFractalWeb() {
rootNode = new Node(new THREE.Vector3(0,0,0), 0, 0); rootNode.size = 1.6; nodes.push(rootNode);
const branches=6, maxDepth=4;
const createBranch=(startNode,direction,depth,strength,scale)=>{
if(depth>maxDepth) return;
const endPos=startNode.position.clone().add(direction.clone().multiplyScalar(5*scale));
const newNode=new Node(endPos,depth,depth===maxDepth||Math.random()<0.3?1:0);
newNode.distanceFromRoot=rootNode.position.distanceTo(endPos); nodes.push(newNode);
startNode.addConnection(newNode,strength);
if(depth<maxDepth) for(let i=0;i<3;i++){
const angle=(i/3)*Math.PI*2;
const perp1=new THREE.Vector3(-direction.y,direction.x,0).normalize();
const perp2=direction.clone().cross(perp1).normalize();
const newDir=direction.clone().add(perp1.clone().multiplyScalar(Math.cos(angle)*0.7)).add(perp2.clone().multiplyScalar(Math.sin(angle)*0.7)).normalize();
createBranch(newNode,newDir,depth+1,strength*0.7,scale*0.75);
}
};
for(let i=0;i<branches;i++){
const phi=Math.acos(1-2*(i+0.5)/branches), theta=Math.PI*(1+Math.sqrt(5))*i;
const dir=new THREE.Vector3(Math.sin(phi)*Math.cos(theta),Math.sin(phi)*Math.sin(theta),Math.cos(phi)).normalize();
createBranch(rootNode,dir,1,0.9,1.0);
}
}
switch(formationIndex%3) {
case 0: generateCrystallineSphere(); break;
case 1: generateHelixLattice(); break;
case 2: generateFractalWeb(); break;
}
return { nodes, rootNode };
}
let nodesMesh = null, connectionsMesh = null, neuralNetwork = null;
function createNetworkVisualization(formationIndex, densityFactor = 1.0) {
if (nodesMesh) { scene.remove(nodesMesh); nodesMesh.geometry.dispose(); nodesMesh.material.dispose(); }
if (connectionsMesh) { scene.remove(connectionsMesh); connectionsMesh.geometry.dispose(); connectionsMesh.material.dispose(); }
neuralNetwork = generateNeuralNetwork(formationIndex, densityFactor);
const palette = colorPalettes[config.activePaletteIndex];
// Nodes
const nodesGeo = new THREE.BufferGeometry();
const nodePositions=[], nodeTypes=[], nodeSizes=[], nodeColors=[], distancesRoot=[], nodeFreqBands=[];
neuralNetwork.nodes.forEach(node => {
nodePositions.push(node.position.x, node.position.y, node.position.z);
nodeTypes.push(node.type); nodeSizes.push(node.size); distancesRoot.push(node.distanceFromRoot);
const colorIdx = Math.min(node.level, palette.length-1);
const baseColor = palette[colorIdx].clone();
baseColor.offsetHSL(THREE.MathUtils.randFloatSpread(0.03), THREE.MathUtils.randFloatSpread(0.08), THREE.MathUtils.randFloatSpread(0.08));
nodeColors.push(baseColor.r, baseColor.g, baseColor.b);
// Assign frequency band by layer:
// level 0-1 (core/inner) → bass (0)
// level 2-3 (middle) → mid (1)
// level 4+ (outer/leaf) → high (2)
const band = node.level <= 1 ? 0 : node.level <= 3 ? 1 : 2;
nodeFreqBands.push(band);
});
nodesGeo.setAttribute('position', new THREE.Float32BufferAttribute(nodePositions, 3));
nodesGeo.setAttribute('nodeType', new THREE.Float32BufferAttribute(nodeTypes, 1));
nodesGeo.setAttribute('nodeSize', new THREE.Float32BufferAttribute(nodeSizes, 1));
nodesGeo.setAttribute('nodeColor', new THREE.Float32BufferAttribute(nodeColors, 3));
nodesGeo.setAttribute('distanceFromRoot', new THREE.Float32BufferAttribute(distancesRoot, 1));
nodesGeo.setAttribute('freqBand', new THREE.Float32BufferAttribute(nodeFreqBands, 1));
const nodeMat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(pulseUniforms),
vertexShader: nodeShader.vertexShader,
fragmentShader: nodeShader.fragmentShader,
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
});
nodesMesh = new THREE.Points(nodesGeo, nodeMat);
scene.add(nodesMesh);
// Connections
const connGeo = new THREE.BufferGeometry();
const startPts=[], endPts=[], connStrengths=[], connColorsArr=[], pathIndices=[], positions=[], connFreqBands=[];
let pIdx=0; const processed=new Set();
neuralNetwork.nodes.forEach((node,i) => {
node.connections.forEach(conn => {
const j=neuralNetwork.nodes.indexOf(conn.node); if(j===-1) return;
const key=[Math.min(i,j),Math.max(i,j)].join('-');
if(!processed.has(key)){
processed.add(key);
// Connection's band = average of the two endpoint bands
const bandA = node.level <= 1 ? 0 : node.level <= 3 ? 1 : 2;
const bandB = conn.node.level <= 1 ? 0 : conn.node.level <= 3 ? 1 : 2;
const connBand = Math.round((bandA + bandB) / 2);
for(let s=0;s<20;s++){
const t=s/19;
positions.push(t,0,0);
startPts.push(node.position.x,node.position.y,node.position.z);
endPts.push(conn.node.position.x,conn.node.position.y,conn.node.position.z);
pathIndices.push(pIdx); connStrengths.push(conn.strength);
connFreqBands.push(connBand);
const avgLvl=Math.min(Math.floor((node.level+conn.node.level)/2),palette.length-1);
const col=palette[avgLvl].clone(); col.offsetHSL(THREE.MathUtils.randFloatSpread(0.03),THREE.MathUtils.randFloatSpread(0.08),THREE.MathUtils.randFloatSpread(0.08));
connColorsArr.push(col.r,col.g,col.b);
}
pIdx++;
}
});
});
connGeo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
connGeo.setAttribute('startPoint', new THREE.Float32BufferAttribute(startPts, 3));
connGeo.setAttribute('endPoint', new THREE.Float32BufferAttribute(endPts, 3));
connGeo.setAttribute('connectionStrength', new THREE.Float32BufferAttribute(connStrengths, 1));
connGeo.setAttribute('connectionColor', new THREE.Float32BufferAttribute(connColorsArr, 3));
connGeo.setAttribute('pathIndex', new THREE.Float32BufferAttribute(pathIndices, 1));
connGeo.setAttribute('freqBand', new THREE.Float32BufferAttribute(connFreqBands, 1));
const connMat = new THREE.ShaderMaterial({
uniforms: THREE.UniformsUtils.clone(pulseUniforms),
vertexShader: connectionShader.vertexShader,
fragmentShader: connectionShader.fragmentShader,
transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
});
connectionsMesh = new THREE.LineSegments(connGeo, connMat);
scene.add(connectionsMesh);
// Seed pulse colours from palette
[nodeMat, connMat].forEach(m => {
palette.slice(0,3).forEach((c,i) => { m.uniforms.uPulseColors.value[i].copy(c); });
});
}
// =====================================================================
// PULSE ON CLICK
// =====================================================================
const raycaster = new THREE.Raycaster(), pointer = new THREE.Vector2();
const interactionPlane = new THREE.Plane(new THREE.Vector3(0,0,1), 0);
const interactionPoint = new THREE.Vector3();
let lastPulseIndex = 0;
function triggerPulse(clientX, clientY) {
pointer.x = (clientX/window.innerWidth)*2-1;
pointer.y = -(clientY/window.innerHeight)*2+1;
raycaster.setFromCamera(pointer, camera);
interactionPlane.normal.copy(camera.position).normalize();
interactionPlane.constant = -interactionPlane.normal.dot(camera.position) + camera.position.length()*0.5;
if (raycaster.ray.intersectPlane(interactionPlane, interactionPoint) && nodesMesh && connectionsMesh) {
lastPulseIndex = (lastPulseIndex+1)%3;
const t = clock.getElapsedTime();
nodesMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(interactionPoint);
nodesMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = t;
connectionsMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(interactionPoint);
connectionsMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = t;
const randCol = colorPalettes[config.activePaletteIndex][Math.floor(Math.random()*colorPalettes[config.activePaletteIndex].length)];
nodesMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(randCol);
connectionsMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(randCol);
}
}
renderer.domElement.addEventListener('click', (e) => {
if (e.target.closest('.morph-button') || e.target.closest('#audio-controls')) return;
triggerPulse(e.clientX, e.clientY);
});
renderer.domElement.addEventListener('touchstart', (e) => {
if (e.target.closest('.morph-button') || e.target.closest('#audio-controls')) return;
if (e.touches.length) triggerPulse(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
// Morph button
document.getElementById('change-formation-btn').addEventListener('click', () => {
config.currentFormation = (config.currentFormation+1) % config.numFormations;
createNetworkVisualization(config.currentFormation, config.densityFactor);
controls.autoRotate = false;
setTimeout(() => { controls.autoRotate = true; }, 2500);
});
// =====================================================================
// BEAT DETECTION STATE
// =====================================================================
let beatPrevBass = 0, beatPrevHigh = 0;
// Fire a pulse from an exact world-space position (used by beat detection)
function triggerPulseAt(worldPos, bandHint) {
if (!nodesMesh || !connectionsMesh) return;
lastPulseIndex = (lastPulseIndex + 1) % 3;
const t = clock.getElapsedTime();
nodesMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(worldPos);
nodesMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = t;
connectionsMesh.material.uniforms.uPulsePositions.value[lastPulseIndex].copy(worldPos);
connectionsMesh.material.uniforms.uPulseTimes.value[lastPulseIndex] = t;
// Colour: bass pulses → warm purple, high pulses → cool blue-white
const palette = colorPalettes[config.activePaletteIndex];
const col = bandHint === 0
? palette[0] // bass = deep purple/blue from palette
: palette[2]; // high = mid-palette colour
nodesMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(col);
connectionsMesh.material.uniforms.uPulseColors.value[lastPulseIndex].copy(col);
}
// =====================================================================
// ANIMATION LOOP
// =====================================================================
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
// Get audio data
const audio = getAudio(t);
// Update progress bar
if (isPlaying && audioCtx) {
const cur = Math.min(audioCtx.currentTime - startTime, audioDuration);
progressFill.style.width = (audioDuration > 0 ? (cur/audioDuration)*100 : 0) + '%';
timeCur.textContent = fmtTime(Math.max(0, cur));
}
// Push audio to both meshes' uniforms
if (nodesMesh) {
const u = nodesMesh.material.uniforms;
u.uTime.value = t;
u.uBass.value = audio.bass;
u.uMid.value = audio.mid;
u.uHigh.value = audio.high;
u.uEnergy.value = audio.energy;
// Subtle network-wide rotation breathe on bass
nodesMesh.rotation.y = Math.sin(t * 0.04) * 0.05 + audio.bass * 0.04;
}
if (connectionsMesh) {
const u = connectionsMesh.material.uniforms;
u.uTime.value = t;
u.uBass.value = audio.bass;
u.uMid.value = audio.mid;
u.uHigh.value = audio.high;
u.uEnergy.value = audio.energy;
connectionsMesh.rotation.y = Math.sin(t * 0.04) * 0.05 + audio.bass * 0.04;
}
// Bloom: more responsive now — band-separated reactivity keeps detail
bloomPass.strength = 1.6 + audio.bass * 0.8 + audio.energy * 0.3;
bloomPass.threshold = Math.max(0.5, 0.72 - audio.energy * 0.2);
// ---- BEAT-TRIGGERED AUTO PULSES ----
// Fire a pulse from a random point in the net whenever a transient
// is detected on bass or high frequencies.
if (isPlaying && nodesMesh && connectionsMesh && neuralNetwork) {
// Bass transient: smoothed value jumped significantly vs last frame
if (sBass > beatPrevBass + 0.042 && sBass > 0.07) {
// Pick a random inner node (bass zone) as origin
const innerNodes = neuralNetwork.nodes.filter(n => n.level <= 1);
const origin = innerNodes[Math.floor(Math.random() * innerNodes.length)];
if (origin) triggerPulseAt(origin.position, 0);
}
// High transient: shimmer pulse from a random outer node
if (sHigh > beatPrevHigh + 0.028 && sHigh > 0.05) {
const outerNodes = neuralNetwork.nodes.filter(n => n.level >= 4);
if (!outerNodes.length) outerNodes.push(...neuralNetwork.nodes.filter(n => n.level >= 3));
const origin = outerNodes[Math.floor(Math.random() * outerNodes.length)];
if (origin) triggerPulseAt(origin.position, 2);
}
beatPrevBass = sBass;
beatPrevHigh = sHigh;
}
// Starfield: high freq drives twinkle, bass spins faster
starField.rotation.y += 0.0002 + audio.bass * 0.003;
starField.material.uniforms.uTime.value = t;
starField.material.uniforms.uHighFreq.value = audio.high;
// Auto-rotate speed gently scales with energy
controls.autoRotateSpeed = 0.22 + audio.energy * 1.5;
controls.update();
composer.render();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
bloomPass.resolution.set(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);
createNetworkVisualization(config.currentFormation, config.densityFactor);
animate();
</script>
</body>
</html>