Skip to content

Particles

Source: particles.h

FrameTime

class FrameTime
src/light/particles.h:74

Converts real elapsed time into a per-frame scale factor, so physics runs at the same SPEED on every target while still using every frame the hardware can render.

The bug this prevents, and the trap next to it. A pool advanced by a fixed amount each frame has physics that are a property of the HARDWARE: the desktop renders tens of thousands of frames a second and an ESP32 a few hundred, so one gravity setting is an explosion on one and a drift on the other. The obvious fix — run a fixed 60 Hz simulation and skip the frames in between — is WRONG for a light effect: it throws away exactly the smoothness the extra frames were rendered for, so a fast device shows 60 Hz motion drawn 5000 times instead of motion 80x smoother.

So time is not quantised, it is SCALED. Every force and velocity is expressed per reference frame (1/60 s), and scale() reports how much of a reference frame this frame covered in 8.8 fixed point: 256 at exactly 60 fps, 26 at 600 fps, 2560 after a 16-frame stall. A faster device therefore takes many small steps where a slow one takes a few large ones — same trajectory, more resolution along it.

Public Methods

inline explicit FrameTime(uint16_t referenceHz = 60) : referenceHz is the rate the effect's numbers are written against; 60 is the convention.

inline uint32_t advance(uint32_t nowMs) : Call once per frame. Returns the scale in 8.8 fixed point (256 == one reference frame).

inline void reset()

Public Static Attributes

constexpr uint32_t kOne = 256 : one reference frame

constexpr uint32_t kMaxScale = 256 * 8

Pool

struct Pool
src/light/particles.h:201

Public Attributes

draw::pos_t * x = nullptr

draw::pos_t * y = nullptr

draw::pos_t * vx = nullptr

draw::pos_t * vy = nullptr

uint16_t * ttl = nullptr : Lifetime in reference frames; 0 = dead.

uint8_t * hue = nullptr : palette index per particle

uint8_t * size = nullptr : Per-particle radius in WHOLE pixels, 0 = a single sub-pixel splat.

uint8_t * acc = nullptr : Sub-unit force accumulator, 3.4 fixed point, one nibble per axis (low = x, high = y).

uint16_t count = 0 : how many slots the arrays hold

uint32_t ageCarry_ = 0 : sub-frame aging remainder (see age())

int64_t gCarry_ = 0 : sub-unit gravity remainder (see gravity())

Public Methods

inline bool valid() const

inline void clear() : Kill every particle — the state a pool starts in, and what prepare() should leave behind.

inline uint16_t findFree() const : Index of a free slot, or count when the pool is full.

inline bool spawn(draw::pos_t px, draw::pos_t py, draw::pos_t svx, draw::pos_t svy, uint16_t life, uint8_t colour, uint8_t radius = 0) : Bring one particle to life.

inline void gravity(draw::pos_t g, uint32_t scale = FrameTime::kOne) : Constant acceleration, the usual case being gravity.

inline void force(draw::pos_t fx, draw::pos_t fy, uint32_t scale = FrameTime::kOne) : A constant push in any direction — wind, a tilt control, a thrust.

inline void forceSmall(int8_t fx, int8_t fy) : A force too SMALL to move a velocity by one unit per frame, accumulated until it does.

inline void drag(uint8_t k, uint32_t scale = FrameTime::kOne) : Velocity damping: v *= (256 - k) / 256.

inline void attract(draw::pos_t ax, draw::pos_t ay, int32_t strength) : Pull every particle toward a point with an inverse-square falloff, clamped near the centre so a particle sitting on the attractor does not receive an unbounded impulse.

inline void step(uint32_t scale = FrameTime::kOne) : Advance every live particle by one frame: position from the CURRENT velocity.

inline void stepDriven(uint32_t scale, bool audioReactive, uint16_t live) : step() with a PER-PARTICLE time scale: drive(i) returns particle i's own multiplier of FrameTime::kOne.

template<typename Drive> inline void stepEach(uint32_t scale, Drive drive)

inline void age(uint16_t rate = 1, uint32_t scale = FrameTime::kOne) : Count down every particle's life; a particle reaching zero is dead and its slot is reusable.

inline void bounce(draw::pos_t w, draw::pos_t h, uint16_t e, uint8_t roughness = 0, uint32_t seed = 0) : Reflect particles off the walls of a w by h grid, keeping a fraction e of the speed (restitution: 256 is a perfect bounce, 0 stops dead).

inline void wrap(draw::pos_t w, draw::pos_t h, bool wrapX = true, bool wrapY = true) : Wrap particles around the grid edges: a particle leaving one side re-enters the other.

inline void killOutside(draw::pos_t w, draw::pos_t h, draw::pos_t margin = 0) : Kill any particle that has left the w by h grid — the alternative to bouncing, for fountains and sparks that should simply be gone once they fall off.

inline void angleEmit(draw::pos_t px, draw::pos_t py, angle16 angle, draw::pos_t speed, angle16 cone, uint8_t n, uint16_t life, uint8_t colour, uint32_t seed) : Emit n particles from a point in a cone around angle, at speed ± spread.

inline void spray(draw::pos_t px, draw::pos_t py, draw::pos_t speed, uint8_t n, uint16_t life, uint8_t colour, uint32_t seed) : Emit n particles from a point with random velocities inside a box — a fountain, a spray, a burst of confetti.

inline void collide(draw::pos_t radius, uint16_t e = 200, uint32_t seed = 0) : Make live particles bounce off each other.

inline void render(const draw::Canvas & cv, uint16_t maxLife = 255, RenderStyle style = RenderStyle::Splat) const : Draw every live particle.

inline uint16_t liveCount() const : How many particles are alive — for a status line, or an effect that tops the pool up.

Public Static Methods

static inline draw::pos_t wrapCoord(draw::pos_t v, draw::pos_t span) : Reduce one coordinate into 0..span, in constant time.