FoldParticles · Fold.page
A GPU-based particle engine with configurable global behavior and per-element DOM-driven attractors, repellers, orbits, and vortices. All 1 200 particles run entirely on the GPU via GPGPU ping-pong render targets — zero CPU loops per frame.
Fold.init({ particles: { count: 1200, // total GPU particles modes: ['brownian', 'orbit'], boundsRadius: 0.85, // world units (≈ 1215 CSS px) boundsStrength: 0.25, friction: 0.84, brownianStrength: 0.0004, attractorBaseStrength: 0.006, // scaled by per-element strength attr depthColorStops: [ { depth: -0.25, color: [0.05, 0.15, 0.90] }, // deep blue { depth: 0.00, color: [0.30, 0.85, 1.00] }, // cyan { depth: 0.25, color: [0.90, 0.45, 1.00] }, // violet ], }, });
A spring force draws nearby particles toward the element's center. Strength and radius are tunable per element.
<div data-foldpage-particles="attract" data-foldpage-particles-strength="0.9" data-foldpage-particles-radius="200"> </div>
An inverse spring force pushes particles away — useful for hollow regions or interactive obstacle effects.
<div data-foldpage-particles="repel" data-foldpage-particles-strength="0.8" data-foldpage-particles-radius="180"> </div>
A tangential force in the XZ plane circles particles around the element's Y axis with a gentle inward spring to maintain distance.
<div data-foldpage-particles="orbit" data-foldpage-particles-strength="0.7" data-foldpage-particles-radius="210"> </div>
XZ tangential spin combined with vertical pull toward the element — creates a funnel or drain effect that pulls particles into a column.
<div data-foldpage-particles="vortex" data-foldpage-particles-strength="0.75" data-foldpage-particles-radius="190"> </div>
contain
Particles move freely inside the element's bounds. Any particle that
crosses the boundary receives a pushback force proportional to how far
it has overshot — fading to zero at radius pixels outside.
<div data-foldpage-particles="contain" data-foldpage-particles-strength="1.5" data-foldpage-particles-radius="80" data-foldpage-particles-emit="200"> </div>
A localized noise field — particles within the influence radius receive randomized impulses. Strength and frequency are tunable per element, independent of the global brownian mode.
<div data-foldpage-particles="brownian" data-foldpage-particles-radius="200" data-foldpage-particles-brownian-strength="0.0012" data-foldpage-particles-brownian-frequency="20"> </div>
A directional gravity field within the influence radius. The vector sets direction and magnitude — particles outside the radius are unaffected. Force falls off quadratically toward the edge.
<div data-foldpage-particles="gravity" data-foldpage-particles-radius="210" data-foldpage-particles-gravity="0,-0.0004,0"> </div> <!-- combine with other modes --> <div data-foldpage-particles="attract gravity" data-foldpage-particles-gravity="0,-0.0003,0"> </div>
Each particle is spring-linked to the next in the simulation texture,
forming flowing ribbons and streamer effects. A longer
trailLength
makes the ribbon structure more visible. Global simulation mode — set via Fold.init.
Fold.init({ particles: { modes: ['chain'], chainStrength: 0.002, // spring between adjacent particles trailLength: 12, // longer trails show ribbon structure friction: 0.92, }, });
Flocking simulation — particles attract nearby neighbours (cohesion),
align velocities (alignment), and avoid crowding (separation).
Composing with brownian
breaks up lock-step synchronisation. Global simulation mode — set via Fold.init.
Fold.init({ particles: { modes: ['brownian', 'boid'], boidRadius: 0.05, // cohesion + alignment radius boidSepRadius: 0.03, // separation avoidance radius boidCohesionStr: 0.008, boidAlignmentStr: 0.005, boidSeparationStr: 0.003, }, });
Tangential XZ force combined with sinusoidal vertical oscillation —
particles trace helical paths around
springTarget.
Raise spiralVertStr
for tighter vertical coils. Global simulation mode — set via Fold.init.
Fold.init({ particles: { modes: ['spiral'], spiralTangentStr: 0.0001, // XZ tangential force spiralVertStr: 0.02, // vertical oscillation amplitude springTarget: [0, 0, 0], springStrength: 0.00005, }, });
When an element lists multiple modes, each parameter attribute accepts a space-separated list aligned positionally to the mode tokens — so attract and brownian can have different strengths, radii, and friction values.
<div data-foldpage-particles="attract brownian" data-foldpage-particles-strength="1.0 0.3" data-foldpage-particles-radius="180 360" data-foldpage-particles-emit="120" data-foldpage-particles-brownian-strength="0 0.0006" data-foldpage-particles-friction="0.96 0.88"> </div>
Global Scope
data-foldpage-particles-target="body"
promotes an element from a local attractor to a global config source.
The primary attribute sets simulation modes; other attrs override global physics.
No Fold.init() call is needed.
The hidden element below sets orbit + brownian simulation modes,
friction 0.93, and a mild upward gravity
— identical to the equivalent Fold.init call shown on the right.
<!-- target="body": element becomes a global config source. data-foldpage-particles sets simulation modes here, not attractor behaviors. No Fold.init() required. --> <div data-foldpage-particles="orbit brownian" data-foldpage-particles-target="body" data-foldpage-particles-friction="0.93" data-foldpage-particles-brownian-strength="0.00015" data-foldpage-particles-gravity="0,0.00003,0"> </div> <!-- Equivalent Fold.init() call: --> Fold.init({ particles: { modes: ['orbit', 'brownian'], friction: 0.93, brownianStrength: 0.00015, gravity: [0, 0.00003, 0], }, });
Composed · Demo 1
vortex spins particles around the element's Y‑axis.
gravity applies a separate downward pull across a wider radius.
The two forces compete — particles orbit but lose altitude, spiraling downward like water draining into a vortex.
Each mode has its own tuned radius, strength and friction so the spin and the fall operate at independent scales.
<!-- vortex spins tight (r=140); gravity field is wider (r=340) and pulls harder — creating a downward drain spiral. Per-mode gravity uses semicolons to separate vectors. --> <div data-foldpage-particles="vortex gravity" <!-- vortex: strong, tight gravity: softer, wide --> data-foldpage-particles-strength="1.8 0.55" data-foldpage-particles-radius="140 340" <!-- emit/lifetime apply to the element, not per-mode --> data-foldpage-particles-emit="220" data-foldpage-particles-lifetime="7" <!-- per-mode gravity vectors (semicolon = mode separator): vortex slot: gentle downward nudge gravity slot: stronger pull that dominates at distance --> data-foldpage-particles-gravity="0,-0.0006,0; 0,-0.0028,0" <!-- vortex particles keep momentum; gravity drains it faster --> data-foldpage-particles-friction="0.945 0.905"> </div>
Composed · Demo 2
contain creates an invisible boundary at the element's edges, bouncing particles back with a short, sharp falloff.
brownian fills a wider zone with chaotic turbulence to agitate the trapped swarm.
The result is a snow-globe effect — particles scramble frantically inside the cage but can't escape.
Contain gets tight friction so collisions feel elastic; brownian gets looser friction so jitter accumulates.
<!-- contain walls particles in with a 28 px falloff edge. brownian fills a huge radius (380 px) with turbulence. The cage traps; the turbulence keeps them pinballing. --> <div data-foldpage-particles="contain brownian" <!-- contain: very strong wall brownian: light nudges --> data-foldpage-particles-strength="2.2 0.15" <!-- contain: tight 28 px edge brownian: wide 380 px field --> data-foldpage-particles-radius="28 380" <!-- emit/lifetime apply to the element, not per-mode --> data-foldpage-particles-emit="260" <!-- brownian params apply to brownian slot only --> data-foldpage-particles-brownian-strength="0 0.0018" data-foldpage-particles-brownian-frequency="0 4.5" <!-- contain bounces elastically; brownian lets energy build --> data-foldpage-particles-friction="0.82 0.94"> </div>
Emitters
Adding data-foldpage-particles-emit="N"
initialises a dedicated slice of N particles near the element's position at
mount time. The element also acts as an attractor to keep its particles in the
neighbourhood. Multiple emitters split the particle count between them.
<!-- Each element seeds N additional particles --> <div data-foldpage-particles="attract" data-foldpage-particles-strength="0.6" data-foldpage-particles-radius="120" data-foldpage-particles-emit="150" data-foldpage-particles-lifetime="4"> emitter A </div> <!-- lifetime overrides particleLifetime for this emitter's particles. 0 or omitted = immortal / use global setting. -->
Cursor / Touch
When interactiveStrength
is set, particles are attracted toward the cursor position in real time.
Move the mouse across the scene to pull the field — no DOM element required.
interactiveDistance
controls how close (in world units) the cursor must be, and
interactiveStaleDecay
fades the force when the pointer is idle.
Fold.init({ particles: { // attract toward cursor / touch interactiveStrength: 0.002, // force magnitude interactiveDistance: 0.05, // world-unit radius interactiveStaleDecay: 0.99, // fade when pointer idle }, });
Fold.init({ particles: { // Simulation count: 3000, // GPU particle count modes: ['brownian', 'orbit'], // global simulation modes friction: 0.78, // velocity damping per frame brownianStrength: 0.0005, // random impulse magnitude gravity: [0, -0.00004, 0], springStrength: 0.00001, springTarget: [0, 0, 0], // Bounds boundsShape: 'box', // 'box' (default) | 'sphere' boundsAutoDoc: true, // auto-size box to document size; updates on resize boundsBox: [0.56, 1.4, 0.45], // explicit half-extents [x,y,z]; overrides boundsAutoDoc boundsCenter: [0, -0.7, 0], // explicit center; overrides boundsAutoDoc boundsRadius: 0.45, // sphere radius, or Z depth half-extent for box boundsStrength: 1, boundsMode: 'constrain', // 'constrain' | 'wrap' wrapDamping: 0, // Interactivity (cursor / touch) interactiveStrength: 0.002, interactiveDistance: 0.05, interactiveStaleDecay: 0.99, // DOM element attractors maxAttractors: 16, // max simultaneous elements (hard cap: 16) attractorBaseStrength: 0.004, // scaled by data-attr strength value // Rendering trailLength: 4, // frames back for trail endpoint lineThickness: 1.0, // pixels (screen-space) blendMode: 'normal', // 'normal' | 'additive' | 'multiply' | 'subtractive' depthColorStops: [ { depth: -0.2, color: [0.05, 0.1, 1.0] }, { depth: 0.0, color: [0.2, 1.0, 0.4] }, { depth: 0.2, color: [1.0, 0.1, 0.05] }, ], // Advanced modes chainStrength: 0.002, boidRadius: 0.05, spiralTangentStr: 0.0001, }, });
<!-- Behavior mode — marks element as a particle influencer --> data-foldpage-particles="attract" // spring pull toward element centre data-foldpage-particles="repel" // spring push away from element centre data-foldpage-particles="orbit" // 3D tangential spin (adapts to angular momentum) data-foldpage-particles="vortex" // XZ spin + vertical pull (funnel/drain) data-foldpage-particles="contain" // keep particles inside element bounds data-foldpage-particles="brownian" // localized turbulence field data-foldpage-particles="gravity" // localized directional gravity field <!-- Multiple modes: space- or comma-separated (each uses one attractor slot) --> data-foldpage-particles="attract brownian" data-foldpage-particles="vortex,gravity" <!-- Per-element tuning --> data-foldpage-particles-strength="0.5" // 0–1, scaled by attractorBaseStrength data-foldpage-particles-radius="150" // influence radius in CSS px (all modes) data-foldpage-particles-emit="100" // seed N particles near this element data-foldpage-particles-lifetime="3.5" // emitter lifetime in seconds (0 = immortal) <!-- Brownian mode tuning (default: global brownianStrength / brownianFrequency) --> data-foldpage-particles-brownian-strength="0.001" // turbulence magnitude data-foldpage-particles-brownian-frequency="20" // step rate (higher = faster jitter) <!-- Gravity mode tuning (default: global gravity vector) --> data-foldpage-particles-gravity="0,-0.0003,0" // x,y,z direction + magnitude <!-- Namespace follows Fold.init({ namespace: '...' }) --> <!-- e.g. namespace:'myapp' → data-myapp-particles="attract" -->
Each pair carries only the mode attribute. Strength, radius, emit, and friction all
derive from the element's rendered area via
sizeFactor = clamp(√(area ÷ 60 000 px²), 0.1, 3.0).
small
large
<div data-foldpage-particles="attract"></div>
small
large
<div data-foldpage-particles="repel"></div>
small
large
<div data-foldpage-particles="orbit"></div>
small
large
<div data-foldpage-particles="vortex"></div>
small
large
<div data-foldpage-particles="contain"></div>
small
large
<div data-foldpage-particles="brownian"></div>
small
large
<div data-foldpage-particles="gravity" data-foldpage-particles-gravity="0,-0.001,0"></div>
Auto-Emit
Set
count: 0
in Fold.init to suppress the global ambient cloud, then mark
DOM elements with a mode attribute. Without an explicit
emit
attribute each element auto-seeds particles proportional to its area.
No particle counts to manage — elements bring their own.
Fold.init({ particles: { count: 0, // no ambient cloud friction: 0.89, trailLength: 7, }, }); <!-- No emit attr → auto-seeds per element area --> <div data-foldpage-particles="attract">card A</div> <div data-foldpage-particles="orbit">card B</div> <div data-foldpage-particles="brownian">card C</div> <!-- auto-emit = round(200 × sizeFactor) per element --> <!-- use emit="0" to suppress a specific element -->
particles: [{…}]
Pass particles
as an array to create independent particle systems. Each entry is scoped to a DOM element
via target
(CSS selector or HTMLElement) — bounds, attractor scanning, and world positioning are all
isolated per group. An entry without target
remains the global full-page system.
Fold.init({ particles: [ { /* global ambient system — no target */ }, { target: '#particles-zone-a', // CSS selector or HTMLElement count: 200, modes: ['orbit', 'brownian'], }, { target: '#particles-zone-b', count: 200, modes: ['boid', 'brownian'], }, ], });
Zone A · #particles-zone-a
Three attract nodes inside the target element. The
orbit
global mode spins all particles around the zone centre; each node also draws nearby particles inward.
Zone B · #particles-zone-b
A central repel hollows out the flock; attract nodes at either side anchor it.
The boid
global mode drives cohesion, alignment, and separation between all particles in the zone.