Skip to component
Modulo

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-infection

Requires 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 video

Interaction

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

PropTypeDefault
inactivityDelayPointer inactivity in milliseconds before emergence.number2400
fullInfectionDelayMilliseconds to full infection. Thin-root consolidation starts halfway through, after at least eight seconds.number24000
maxRootsOptional allocation budget across all phases. Mature growth reuses absorbed branch slots.numberInfinity
maxParticlesOptional particle budget. Omit for an expanding pool. Finite budgets have a minimum of 16.numberInfinity
growthSpeedGrowth multiplier. Clamped to 0.1 through 4.number1
branchingProbabilityBranch probability per growth interval. Clamped to 0 through 0.5. Generations spread breadth-first.number0.075
physicsIterationsDistance constraint passes per fixed step. Clamped to 1 through 12.number5
particleRadiusStarting radius in CSS pixels. Roots vary, taper, and keep thickening without a size ceiling.number4.2
enableAudioMute all procedural audio when false. Changes apply immediately.booleantrue
volumeMaster volume from 0 through 1. Changes apply immediately.number0.45
enableExplosionPrimary pointer press releases fragments with outward impulses and gravity.booleantrue
enableRetractionMeaningful pointer movement or keyboard interaction retracts the roots.booleantrue
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) => voidOptional
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 inspiration

Source

Checking source access...