Root Infection
Stay still. Roots grow from your cursor and spread across the page.
Preview
don't move your mouse
Installation
bunx shadcn@latest add @modulo/root-infectionRequires an active Modulo key and a project configured for shadcn.
Registry setup
Set MODULO_REGISTRY_KEY to your key. Add this registry entry to components.json.
{
"registries": {
"@modulo": {
"url": "<registry-url>/r/{name}.json",
"headers": {
"Authorization": "Bearer ${MODULO_REGISTRY_KEY}"
}
}
}
}Usage
import { RootInfection } from "@/components/root-infection/RootInfection";
export default function Example() {
return <RootInfection inactivityDelay={2400} volume={0.45} />;
}React, WebGL 2 with a Canvas 2D fallback, and Web Audio. No physics or animation package required. All ten source files install together.
Registry JSONDownload thumbnailDownload preview GIFDownload preview videoInteraction
Keep the pointer still in the preview. Move it to pull the roots back, or click to scatter them. Press Escape to clear the screen.
Hold Shift to approach the roots without clearing them. They recoil from the pointer. Shift-drag a root to pull on it. Scrolling pushes them sideways.
Roots cling to buttons and links and tug their edges. As the infection matures, thinner roots merge into larger trunks. Thick new forks keep spreading while the existing roots grow wider and press against the viewport.
Sound starts after the first click or key press. The header sound toggle mutes the demo. Reduced motion keeps the preview still.
Moving outside the preview stops new infections while you read the documentation. The overlay never intercepts links or controls.
API
| Prop | Type | Default |
|---|---|---|
inactivityDelayPointer inactivity in milliseconds before emergence. | number | 2400 |
fullInfectionDelayMilliseconds to full infection. Thin-root consolidation starts halfway through, after at least eight seconds. | number | 24000 |
maxRootsOptional allocation budget across all phases. Mature growth reuses absorbed branch slots. | number | Infinity |
maxParticlesOptional particle budget. Omit for an expanding pool. Finite budgets have a minimum of 16. | number | Infinity |
growthSpeedGrowth multiplier. Clamped to 0.1 through 4. | number | 1 |
branchingProbabilityBranch probability per growth interval. Clamped to 0 through 0.5. Generations spread breadth-first. | number | 0.075 |
physicsIterationsDistance constraint passes per fixed step. Clamped to 1 through 12. | number | 5 |
particleRadiusStarting radius in CSS pixels. Roots vary, taper, and keep thickening without a size ceiling. | number | 4.2 |
enableAudioMute all procedural audio when false. Changes apply immediately. | boolean | true |
volumeMaster volume from 0 through 1. Changes apply immediately. | number | 0.45 |
enableExplosionPrimary pointer press releases fragments with outward impulses and gravity. | boolean | true |
enableRetractionMeaningful pointer movement or keyboard interaction retracts the roots. | boolean | true |
qualityAuto keeps full detail and can advance consolidation when frame cost grows. Low and medium apply only when explicitly selected. | "low" | "medium" | "high" | "auto" | "auto" |
classNameClass names for the preview host. The overlay remains fixed to the viewport. | string | "" |
onReadyStable callback for diagnostics, reset, and audio control. Receives null on unmount. | (controller | null) => void | Optional |
Controller methods
snapshot() reports particle counts, branch counts, solver error, audio state, rendering backend, and measured frame timings. WebGL body timings measure CPU submission, while frame intervals include browser scheduling. reset() clears the infection. setAudio(enabled, volume) changes audio immediately.
Keep onReady stable. Physics prop changes create a fresh simulation. Audio and class changes preserve the current infection. Cleanup calls destroy() automatically.
How it works
Each root is a chain of pooled particles. Verlet integration derives velocity from position history. Distance constraints keep neighboring joints connected, and angular constraints resist sharp folds more strongly as the roots thicken. Branches share a parent joint.
A shared breadth-first frontier fills sibling branch slots before opening a deeper generation. New roots and segments inherit the infection age, so later branches start thicker and occupy more space between joints. Segment additions slow as their size increases.
At the default settings, consolidation starts 12 seconds after emergence. Thin roots, including earlier branches and detached worms, converge onto larger trunks in staggered 1.8-second merges. Substantial forks and their supporting parents stay connected.
Mature branches start with at least 60% of their parent's radius before tapering. A coarse occupancy grid guides new forks into uncovered areas. Crowded leaves merge into their parents to release particles for new growth. Once the viewport fills, thick forks keep pushing toward its edges. Existing chains pull toward their targets before adding more joints.
Merges add cross-sectional tissue once. Mature forks return the tissue added since birth, so their inherited girth does not get counted again on every recycling cycle. Radial growth continues without a size ceiling.
Thickness has no size ceiling. Released particles leave the active iteration list, so physics and rendering process the surviving network. The pool reuses storage on the next infection. Pass finite root and particle budgets to limit allocation. Outward forces and edge oscillations increase with age while the viewport contains the roots.
A fixed 60 Hz physics clock runs separately from interpolated rendering. The solver clamps velocity and catches up at most six steps per frame. A spatial hash limits root collision queries. Cached rectangular distance fields push particles out of controls and steer growing tips around their edges. Mark an element with data-root-obstacle to include it, or data-root-ignore to exclude it.
Retraction slides joints backward along captured root paths with increasing speed. Branch anchors follow their moving parents. An impact cuts the chains into short ligaments, gives them outward and rotational impulses, and releases them under gravity until they fall below the viewport.
Auto quality keeps the full pixel density, configured constraint passes, and all rendering detail. If measured frame cost grows, it can start consolidation after eight seconds. It changes growth timing to reduce the active network. Low and medium quality remain explicit options. Collision proxies stay small enough for local hash queries as visible tissue swells. Hidden tabs stop the clock. Unmount removes the overlay, listeners, observers, timers, and audio resources.
The renderer builds rounded tube meshes whose neighboring joints share surface normals. Connected forks share a skin field, so their texture stays continuous as they merge. WebGL depth-tests the tissue into a floating-point field and adds lighting, pores, veins, small scars, and wet highlights once per visible pixel. The roots use brighter maroon tones on dark pages and near-black tones on light pages. Background sampling updates once a second during the infection, including after a theme change. Browsers without floating-point targets use the same material directly. Canvas 2D provides a fallback. Geometry and GPU buffers are reused.
The sound engine synthesizes filtered noise, small cracks, a beating bass drone, a stereo suction sweep, and a descending sub-bass impact. A compressor and voice limit control overlapping sound.
Physics inspirationSource
Checking source access...