Rössler Attractor
Open the Rössler attractor. Drag with the pointer to examine the phase portrait from any angle. Adjust the bifurcation parameter from 3 to 12 via the slider, or toggle pause/play to freeze or resume the moving trajectory markers.
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 system exhibits rotational oscillations with growing amplitude. 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 single folded band in phase space.
Adjusting transitions the system across period-doubling bifurcations, revealing distinct chaotic bands and periodic orbits.
Numerical Integration via Runge–Kutta 4 (RK4)
Simple Euler integration has first-order truncation error and rapidly diverges in chaotic systems, blurring fine phase-space structure. 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,
];
}
For RK4’s error order, stability region and step-halving checks, see Solving ODEs numerically.
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 spiral and folding structure.
Three.js Geometry and Spectral Tube
Rendering 28,000 discrete points with single-pixel line segments 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 bounding-box center
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 multi-stop spectral vertex colors along arc length
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 temporal order 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 at constant arc-length speed 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: Timestamps cap rendering at 30 FPS to avoid wasting GPU cycles on high-refresh-rate displays.
- Background pausing: Animation-frame requests stop completely when the container is offscreen or the tab is hidden.
- Disposal: On unmount or parameter changes, the component disposes geometries, materials, both
InstancedMeshobjects, and controls; disconnects observers; and disposes the WebGL renderer and releases its context.