LempoLempo Particle FX Docs

Shared Contracts & Export Format

Shared schemas and named data contracts live in one place. Simulation implementations are not all identical: the browser preview has a shared TypeScript engine, while the GDevelop extension keeps a separate runtime.

Single source of truth

contracts/contracts.json — particle record layout, SHAPE_ORDER, easings, blend modes, export schema, and the morph window.

python tools/generate_contracts.py          # regenerate the bindings
python tools/generate_contracts.py --check  # CI drift gate
Edit the JSON, run the generator, never the generated outputs. CI fails on stale files or drifted consumers.

Consumers

ConsumerHow it reads the contracts
Python (editor/particle_studio.py) Imports the generated files directly
C++ core (core/particle_core.cpp) Includes the generated header (contracts/gen/contracts.h)
Browser TypeScript (lempo-runtime/) Shared source for browser-preview particle simulation and pure trail math; renderer adapters consume typed-array frames
GDevelop extensionSeparate simulation implementation that consumes the export format

Export format v1.2

  • Validated against contracts/gen/schema.json on save and load.
  • Easing values are hyphenated: linear / ease-in / ease-out / ease-in-out.
  • migrate_effect() heals old files automatically: missing version, v1.0 and v1.1 → v1.2, camelCase easings, missing blend mode / seed.
  • The effect version is append-only — new fields arrive with defaults (all OFF), so old files always load.

Particle record layout

One flat 22-field record per particle — the exact layout the parity tests compare:

[x, y, vx, vy, age, c0, c1, s0, s1, life, z, vz, shape, tracks,
 dx, dy, dz, gx, gy, gz, sizeRatio, speedRatio]
FieldsMeaning
x, y, zposition (z/vz used in 3D)
vx, vy, vzvelocity
age, lifeage vs lifetime (0..1 drives everything over-life)
c0, c1state colors (birth → death)
s0, s1size keyframes
shape, tracksmorph shape ids + track bits
dx, dy, dzdirection / alignment axis
gx, gy, gzguide/gizmo or per-particle offsets
sizeRatio, speedRatioper-particle spawn draws (seeded RNG)

Determinism

A nonzero seed makes replay deterministic within each runtime. The preview and GDevelop extension consume random values differently, so equal seeds do not guarantee identical particle records between them:

EngineRNG
Python (SimEngine)_rand
C++ coreset_seed
Browser previewmulberry32
GDevelop extensionF.srand / F.rrand
Force fields (v1.2) followed the same recipe: appended with defaults OFF, so legacy motion stays bit-identical for old files. See Testing for the gates that enforce this.