An interactive physics simulation of spinning tops in a circular arena. Watch tops spiral toward the center, collide with realistic physics. Includes OSC (Open Sound Control) support for integration with SuperCollider, PureData, and PuckData.
- Interactive Spawning: Click anywhere in the circular arena to spawn spinning tops
- Realistic Physics:
- Spiral movement toward center based on spin direction
- Elastic collisions with momentum, spin, and gravitational energy
- Velocity decay over time
- Smooth boundary collisions
- Visual Feedback:
- Direction arrows showing movement vector for each top
- Arena-wide white strobe flash on collisions
- Black and white minimalist design
- Auto-Cleanup: Tops automatically disappear when they exit the arena or stop moving
- OSC Support: Send real-time data to sound synthesis software
First, install dependencies:
npm installThen, run the development server:
npm run devOpen http://localhost:3000 with your browser to see the simulation.
- Spawn Tops: Click anywhere inside the circular arena to spawn a spinning top
- Multiple Tops: Keep clicking to add more tops - they will interact with each other
- Select Top: Click on an existing top to select it and view its details (position, velocity, direction vector)
- Watch the Battle: Tops will spiral toward the center, collide with each other, and the arena will flash white on impacts
The simulation sends OSC messages that can be received by SuperCollider, PureData, PuckData, or any OSC-compatible software.
Set environment variables to configure OSC (defaults to localhost on port 57120, compatible with SuperCollider and PureData):
OSC_HOST=127.0.0.1 # OSC server host (default: 127.0.0.1)
OSC_PORT=57120 # OSC server port (default: 57120)Or create a .env.local file:
OSC_HOST=127.0.0.1
OSC_PORT=57120
Note for WSL2 users: The application automatically detects WSL2 environments and attempts to use the Windows host IP address. If automatic detection fails, manually set OSC_HOST to your Windows host IP address.
You can check if OSC is properly configured by visiting:
http://localhost:3000/api/osc
This will return a JSON response with OSC configuration and status information.
Sent when a new top is created.
Arguments:
x(float): Normalized X position (-1 to 1)y(float): Normalized Y position (-1 to 1)
Sent when tops collide (throttled to 20Hz).
Arguments:
intensity(float): Collision intensity (0 to 1)
Sent for each top at 10Hz update rate. One message per top, sent sequentially.
Arguments:
index(int): Top index (0-based)x(float): Normalized X position (-1 to 1)y(float): Normalized Y position (-1 to 1)speed(float): Normalized speed (0 to 1)collisionFlash(float): Collision flash intensity (0 to 1)
Sent at 10Hz with overall simulation state.
Arguments:
topCount(int): Number of active topsarenaFlash(float): Arena flash intensity (0 to 1)
// Receive OSC messages
OSCdef(\spawn, { |msg|
var x = msg[1], y = msg[2];
("Top spawned at: " ++ x ++ ", " ++ y).postln;
}, '/stfs/spawn');
OSCdef(\collision, { |msg|
var intensity = msg[1];
// Trigger sound on collision
{ SinOsc.ar(440 * (1 + intensity), 0, 0.1) * EnvGen.kr(Env.perc(0.01, 0.1), doneAction: 2) }.play;
}, '/collision');
OSCdef(\top, { |msg|
var index = msg[1], x = msg[2], y = msg[3], speed = msg[4], flash = msg[5];
// Map position to panning, speed to frequency
var pan = x;
var freq = 200 + (speed * 800);
// Continuous sound based on top position and speed
}, '/top');- Create an
[udpreceive 57120]object - Connect to
[route /stfs /collision /top /stfs/summary]to route messages - For
/topmessages, use[unpack i f f f f]to unpack arguments (index, x, y, speed, flash) - For
/stfs/spawnmessages, use[unpack f f]to unpack arguments (x, y) - For
/collisionmessages, use[unpack f]to unpack intensity - Map values to your synthesis parameters
// In PuckData
OSC.listen(57120, (msg) => {
if (msg.address === '/collision') {
// Trigger sound on collision
const intensity = msg.args[0];
playSound(440 * (1 + intensity), intensity);
}
if (msg.address === '/top') {
const [index, x, y, speed, flash] = msg.args;
// Map to synthesis parameters
setPan(x);
setFrequency(200 + speed * 800);
setAmplitude(flash);
}
if (msg.address === '/stfs/spawn') {
const [x, y] = msg.args;
// Trigger spawn sound
playSpawnSound(x, y);
}
if (msg.address === '/stfs/summary') {
const [topCount, arenaFlash] = msg.args;
// Use summary data for overall mix control
setMasterVolume(arenaFlash);
}
});- Next.js 16 - React framework
- React 19 - UI library
- Canvas API - 2D rendering
- osc-js - OSC protocol implementation
- Tailwind CSS - Styling
STFS/
├── src/
│ ├── app/
│ │ ├── page.js # Main simulation component
│ │ ├── api/
│ │ │ └── osc/
│ │ │ └── route.js # OSC API endpoint
│ │ └── globals.css # Global styles
│ └── lib/
│ ├── SpinningTop.js # Physics simulation class
│ ├── renderer.js # Canvas rendering utilities
│ └── osc.js # OSC client utilities
├── test-udp.js # UDP/OSC testing utility
└── package.json
MIT license