Splatter Spin is a deterministic, browser-based paint-pendulum simulation. Multiple damped spherical pendulums carry independent draining paint containers and layer their three-dimensional paint trajectories onto one shared canvas.
Requires a recent Node.js release (Node 20.19+ or 22.12+).
npm install
npm run devOpen the local URL printed by Vite. The controls are grouped into Pendulums, Canvas, and Project sections. Select a listed pendulum to edit it live, position its pivot with ground-plane X/Y and vertical Z coordinates, use Add another pendulum to clone it into a new independently editable layer, and remove entries with ×. All listed pendulums run simultaneously when Start is selected.
While stopped, valid pendulum edits immediately rebuild the initial-state preview, including every pivot, arm, paint can, and projected impact. The upper physics view uses an oblique projection so both canvas-plane X and Y placement remain visible. Pendulum controls and list actions are disabled while the simulation is running. Pause to re-enable them; editing a paused run resets the artwork to the revised initial state.
The maximum arm length updates from effective pivot height as Z changes, always reserving 0.05 m of clearance above the ground canvas. Lowering Z clamps an overlong value immediately; raising Z makes the additional length available. The same constraint is enforced when loading JSON.
Starting a fresh or completed run applies the current GUI values automatically. Pausing and starting an in-progress run resumes it without resetting.
Initial inclination and azimuth set the starting arm direction, while orbit speed supplies rotation around the pivot. Paint amount sets the initial reservoir volume, hole diameter controls how quickly it drains, and the logarithmic drop-size slider spans 0.1–100 µl per release.
Before a run starts, dashed rings project the next 10 seconds of calculated impacts from every listed pendulum. They update live as valid controls change and disappear when the real run begins. Preview simulations use a lightweight 30 Hz step, retain pendulum motion, ballistic flight, draining, and seeded variation, reduce dense streams to two representative particles per preview step, and evenly sample at most 500 displayed impacts per pendulum. Starting never waits for projection work; full simulations continue to use 120 Hz physics.
Save JSON downloads a concise project document containing shared project settings, canvas settings, and the pendulum list. Load JSON validates that structure, updates the complete interface and projection, and prepares a paused run. The format is:
{
"version": 1,
"seed": 42,
"durationSeconds": 30,
"canvas": {
"widthMeters": 4.2,
"depthMeters": 4.2,
"color": "#fffdf6"
},
"pendulums": [
{
"paintColor": "#e63946",
"lengthMeters": 1.2,
"initialInclinationDegrees": 40,
"initialAzimuthDegrees": 15,
"initialOrbitSpeedRadiansPerSecond": 1.35,
"dampingPerSecond": 0.015,
"initialPaintMilliliters": 100,
"holeDiameterMillimeters": 1.5,
"dropletVolumeMilliliters": 0.01,
"randomness": 0.45,
"dropSizeVariation": 0.4,
"positionXMeters": 0,
"positionYMeters": 0,
"positionZMeters": 0
}
]
}Pendulum n uses random seed seed + n, preserving stable deterministic output in list order.
Save artwork PNG · 4K redraws only the accumulated painting—not the interface or projection—to a 4096-pixel-wide PNG. The export height is derived from the configured canvas aspect ratio and includes its chosen background color plus every pendulum's full-resolution paint marks.
Useful checks:
npm test
npm run format:check
npm run build
npm run previewThe pendulum stores a Cartesian unit arm direction q and its tangent velocity q′:
q″ = (g/L)(down - (down · q)q) - c q′ - |q′|²q
It is integrated with fourth-order Runge-Kutta at a fixed 1/120-second timestep. After each step, q is normalized and radial velocity is removed to maintain the rigid-arm constraint. Cartesian state avoids the inclination/azimuth singularity when the bob crosses directly beneath the pivot, while RK4 limits artificial energy drift. Rendering uses requestAnimationFrame independently and may perform multiple physics steps per frame.
Each drop begins at the paint container with both of its current horizontal velocity components and zero vertical velocity. It then follows constant-gravity ballistic motion without air resistance. The exact positive root of the height equation determines when it intersects the canvas, avoiding timestep-sized landing errors. A seeded Mulberry32 generator supplies small symmetric variations in mark radius and both landing coordinates.
Reservoir outflow follows Torricelli's law, Q = Cd Ahole √(2gh), using the remaining volume and a fixed container cross-section to determine paint head h. Emitted volume accumulates into user-sized droplets, allowing many droplets to be airborne concurrently even with a fine outlet. Radius scales with the cube root of volume, then receives an independent seeded size variation configured per pendulum; landing scatter remains a separate control. To keep microdroplet streams responsive, dense output is deterministically sampled to at most 24 rendered particles per physics step while the full paint volume is still deducted; as flow slows below that threshold, individual drips are represented one-for-one. This models draining and cadence rather than detailed paint viscosity or surface tension.
The configured duration controls pendulum motion and paint emission. Once it is reached, already-released drops continue until they hit the canvas; only then is the run complete.
This is intentionally a simplified model: the arm is rigid and massless; the bob is a point mass; damping is linear; the paint is treated as an ideal fluid with a fixed discharge coefficient; drops do not collide or experience drag; and marks are stylized rather than simulated fluid splashes. Marks landing outside the finite rectangular canvas are discarded.
Pendulum,PaintSource,PaintDrop,PaintCanvas, andSimulationcontain the model and have no browser dependencies.ProjectConfigis concise serializable data with shared canvas/project settings and a list of pendulum configurations. Internal physical constants are supplied by the model rather than duplicated in saved files.Rendereronly maps model coordinates to the HTML canvas and draws the current state.main.tsowns controls, lifecycle, and the display-frame/fixed-step accumulator.
The production build uses relative asset paths and the included workflow deploys dist on pushes to main.
- Create a GitHub repository and push this project to it.
- Push to
main, or manually run the Deploy to GitHub Pages workflow. - The workflow enables Pages with GitHub Actions on its first run and publishes
diston subsequent runs as well.
No repository-name-specific configuration is required. You can verify the static build locally with npm run build && npm run preview.
Add optional color changes within each reservoir and richer fluid-splash mark shapes.