Rössler Attractor
Open the Rössler attractor. Drag with the pointer to examine the phase portrait from any angle. Adjust the bifurcation parameter via the slider, or toggle pause/play to freeze trajectory progression.
The Dynamical System
Otto Rössler's 1976 paper introduced a simplified continuous system designed to model chaotic behavior with minimal non-linear coupling:
The standard parameters are , , and . In the plane, the dynamics act as a linear harmonic oscillator with an amplitude instability driven by . When exceeds threshold , the non-linear term in the equation rapidly ejects the trajectory into the third dimension before folding it back into the spiral plane—producing a classic folded band attractor.
Adjusting between 3 and 12 transitions the system across period-doubling bifurcations into distinct chaotic bands.
Numerical Integration via Runge–Kutta 4 (RK4)
Simple Euler integration introduces cumulative truncation error that rapidly destroys chaotic phase-space geometry. Instead, the implementation integrates the system using a classical fourth-order Runge–Kutta algorithm with fixed step size :
function derivative(x, y, z, {a, b, c}) {
return [-y - z, x + a * y, b + z * (x - c)];
}
function rk4Step([x, y, z], parameters) {
const {dt} = parameters;
const k1 = derivative(x, y, z, parameters);
const k2 = derivative(
x + (dt * k1[0]) / 2,
y + (dt * k1[1]) / 2,
z + (dt * k1[2]) / 2,
parameters,
);
const k3 = derivative(
x + (dt * k2[0]) / 2,
y + (dt * k2[1]) / 2,
z + (dt * k2[2]) / 2,
parameters,
);
const k4 = derivative(
x + dt * k3[0],
y + dt * k3[1],
z + dt * k3[2],
parameters,
);
return [
x + (dt * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0])) / 6,
y + (dt * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1])) / 6,
z + (dt * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2])) / 6,
];
}
To eliminate initial transient effects, the integration performs 3,000 warm-up steps starting from state before storing subsequent points. It generates 28,000 samples to reconstruct the manifold's folding structure.
Three.js Geometry and Spectral Tube
Rendering 28,000 raw segments directly as lines lacks depth cues and spatial thickness. Instead, the points are subsampled, centered, interpolated into a smooth curve, and extruded as a 3D tube:
const positions = generateRosslerTrajectory({c: parameter, sampleCount: 28000});
const vertices = [];
// Subsample every 6th point (step size 18 in flat array) to balance fidelity and vertex count
for (let i = 0; i < positions.length; i += 18) {
vertices.push(new THREE.Vector3().fromArray(positions, i));
}
// Center the trajectory geometry so rotation pivots around its center of mass
const box = new THREE.Box3().setFromPoints(vertices);
const center = box.getCenter(new THREE.Vector3());
vertices.forEach((v) => v.sub(center));
const curve = new THREE.CatmullRomCurve3(vertices);
const segments = 4800;
const geometry = new THREE.TubeGeometry(curve, segments, 0.12, 5, false);
// Apply vertex colors along a multi-stop instrument spectrum
const SPECTRUM = ['#337dcc', '#28a5a0', '#b1b748', '#ecaa38', '#e26743', '#b766ac', '#337dcc'];
const stops = SPECTRUM.map((c) => new THREE.Color(c));
const colors = new Float32Array(geometry.attributes.position.count * 3);
const color = new THREE.Color();
for (let i = 0; i < colors.length / 3; i++) {
const t = Math.floor(i / 6) / segments * (stops.length - 1);
const k = Math.min(Math.floor(t), stops.length - 2);
color.copy(stops[k]).lerp(stops[k + 1], t - k).toArray(colors, i * 3);
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.MeshBasicMaterial({vertexColors: true});
The color spectrum indicates arc-length progression along the sampled path, helping readers trace the direction of flow along the trajectory ribbon.
Instanced Markers with High-Contrast Outlines
Twelve moving markers circulate along the interpolated trajectory curve to visualize motion. Instead of allocating twelve independent meshes, they are rendered using two THREE.InstancedMesh allocations:
- Inner glowing spheres (
color: '#fff2bc'). - Outer silhouette shells (
color: '#191a16', side: THREE.BackSide, scaled by 1.4).
const beadCount = 12;
const beadGeometry = new THREE.SphereGeometry(0.36, 12, 8);
const beadMaterial = new THREE.MeshBasicMaterial({color: '#fff2bc'});
const outlineMaterial = new THREE.MeshBasicMaterial({color: '#191a16', side: THREE.BackSide});
const beads = new THREE.InstancedMesh(beadGeometry, beadMaterial, beadCount);
const outlines = new THREE.InstancedMesh(beadGeometry, outlineMaterial, beadCount);
// Frustum culling is disabled because instances continuously traverse the full curve
beads.frustumCulled = false;
outlines.frustumCulled = false;
The inverted-normal dark silhouette guarantees that markers remain legible against both dark and light UI themes, even when passing in front of brightly colored sections of the tube.
Lifecycle and Bounds
- Frame pacing:
requestAnimationFramethrottles rendering to ~30 FPS, reducing thermal and power consumption. - Background pausing: Rendering stops completely when off-screen (
IntersectionObserver) or when the tab is hidden (visibilitychange). - Disposal: On unmount or parameter changes, the component disposes geometries, materials, instanced buffers, and forces WebGL context release.