FastLED 3.9.15
Loading...
Searching...
No Matches
perlin_particle_punch.cpp.hpp
Go to the documentation of this file.
2#include "fl/math/math.h"
3#include "noise.h"
4#include "fl/stl/noexcept.h"
5
6namespace fl {
7
8namespace {
9// Clamp a float to [0, 255] before casting to u8, avoiding UB from
10// out-of-range float-to-integer conversion.
11inline u8 clamp_u8(float v) {
12 if (v <= 0.0f) return 0;
13 if (v >= 255.0f) return 255;
14 return u8(v);
15}
16} // namespace
17
18// ---------------------------------------------------------------------------
19// Particle structs
20// ---------------------------------------------------------------------------
21
23 bool alive = false;
24 float position = 0.0f;
25 float velocity = 0.0f;
26 float brightness = 0.0f;
29};
30
32 bool alive = false;
33 float position = 0.0f;
34 float velocity = 0.0f;
35 float intensity = 0.0f;
39
40 float tailLength() const {
41 float len = velocity * 3.0f;
42 if (len < 5.0f)
43 len = 5.0f;
44 if (len > 25.0f)
45 len = 25.0f;
46 return len;
47 }
48
49 bool shouldSpawnDebris() const {
51 (frameCounter % 5 == 0);
52 }
53};
54
56 bool alive = false;
57 float position = 0.0f;
58 float velocity = 0.0f;
59 float brightness = 0.0f;
61};
62
63// ---------------------------------------------------------------------------
64// Construction
65// ---------------------------------------------------------------------------
66
68 mAmbientParticles.resize(50);
69 mMeteorParticles.resize(5);
70 mDebrisParticles.resize(50);
71 mTrailBuffer.resize(num_leds);
72 // Default blue-white palette
73 CRGBPalette16 defaultPalette(CRGB(0, 0, 40), CRGB(0, 40, 120),
74 CRGB(100, 160, 255),
75 CRGB(255, 255, 255));
76 mNoisePalette = defaultPalette;
77 mAmbientPalette = defaultPalette;
78}
79
81
82fl::string PerlinParticlePunch::fxName() const {
83 return "PerlinParticlePunch";
84}
85
86// ---------------------------------------------------------------------------
87// Setters
88// ---------------------------------------------------------------------------
89
93
97
101
103 CRGB tailColor) {
104 mMeteorHeadColor = headColor;
105 mMeteorMidColor = midColor;
106 mMeteorTailColor = tailColor;
107}
108
110 mDrag = drag;
111 // Meteor drag is slightly heavier than ambient.
112 // Scale the difference from 1.0, not the value itself.
113 // drag=0.99 → meteorDrag=0.985, drag=0.80 → meteorDrag=0.74
114 float diff = 1.0f - drag;
115 mMeteorDrag = 1.0f - diff * 1.5f;
116 if (mMeteorDrag < 0.0f)
117 mMeteorDrag = 0.0f;
118}
119
121
125
129
133
135 mMinVelocity = minVel;
136}
137
141
145
146// ---------------------------------------------------------------------------
147// Spawning
148// ---------------------------------------------------------------------------
149
152 if (!p)
153 return;
154 p->alive = true;
155 p->position = 0.0f; // All particles punch out from position 0 (ground)
156 float jitter = float(random8(80, 120)) / 100.0f;
157 p->velocity = (0.5f + intensity * 1.5f) * mSpeed * jitter;
158 p->brightness = 128.0f + intensity * 127.0f;
159 p->paletteIndex = random8();
160 p->headWidth = 3 + random8(3); // 3, 4, or 5
161}
162
163void PerlinParticlePunch::spawnMeteor(float intensity) {
165 if (!m)
166 return;
167 if (intensity < 0.0f)
168 intensity = 0.0f;
169 if (intensity > 1.0f)
170 intensity = 1.0f;
171 m->alive = true;
172 m->position = 0.0f;
173 m->velocity = (1.5f + intensity * 5.0f) * mSpeed;
174 m->intensity = intensity;
175 m->debrisSpawned = 0;
176 m->maxDebris = u8(5 + intensity * 5);
177 m->frameCounter = 0;
178}
179
180// ---------------------------------------------------------------------------
181// Pool allocation
182// ---------------------------------------------------------------------------
183
186 u16 n = (u16)mAmbientParticles.size();
187 for (u16 i = 0; i < n; ++i) {
188 if (!mAmbientParticles[i].alive) {
190 return &mAmbientParticles[i];
191 }
192 }
193 return nullptr;
194}
195
198 u16 n = (u16)mMeteorParticles.size();
199 for (u16 i = 0; i < n; ++i) {
200 if (!mMeteorParticles[i].alive) {
202 return &mMeteorParticles[i];
203 }
204 }
205 return nullptr;
206}
207
210 u16 n = (u16)mDebrisParticles.size();
211 for (u16 i = 0; i < n; ++i) {
212 if (!mDebrisParticles[i].alive) {
214 return &mDebrisParticles[i];
215 }
216 }
217 return nullptr;
218}
219
220// ---------------------------------------------------------------------------
221// Perlin noise (kept from original, with time-warp and palette)
222// ---------------------------------------------------------------------------
223
225 s16x16 out_min, s16x16 out_max) {
226 // Divide first to avoid s16x16 overflow on large intermediate products.
227 // e.g. (223 * 255) exceeds s16x16 max (~32767), but 223/223 * 255 = 255 fits.
228 return (x - in_min) / (in_max - in_min) * (out_max - out_min) + out_min;
229}
230
232 s16x16 sin_val, cos_val;
233 s16x16::sincos(theta, sin_val, cos_val);
234 u32 x =
235 u32((i64(cos_val.raw() + s16x16::SCALE) * 0xafff) >> s16x16::FRAC_BITS);
236 u32 y =
237 u32((i64(sin_val.raw() + s16x16::SCALE) * 0xafff) >> s16x16::FRAC_BITS);
238 // Time dimension with time-warp multiplier
239 u32 z = u32(float(now * 0x000fu) * mTimeMultiplier);
240 u16 val = inoise16(x, y, z);
242 i32((u32(val) << s16x16::FRAC_BITS) / 0xcfffu));
243 constexpr s16x16 one(1.0f);
244 if (tmp > one)
245 tmp = one;
246 tmp = tmp * tmp;
247 tmp = tmp * tmp;
248 tmp = tmp * s16x16(255);
249 return tmp;
250}
251
253 // Apply time-warp to rotation speed — this is the visible acceleration
254 u32 warped_now = u32(float(now) * mTimeMultiplier);
255 s16x16 time_factor = s16x16::from_raw(static_cast<i32>(warped_now * 32u));
256 constexpr s16x16 two_pi(6.2831853f);
257 s16x16 step = two_pi / s16x16(i32(mNumLeds));
258 s16x16 theta = -time_factor;
259 constexpr s16x16 threshold(32.0f);
260 constexpr s16x16 zero(0.0f);
261 constexpr s16x16 max_val(255.0f);
262 for (u16 i = 0; i < mNumLeds; ++i) {
263 s16x16 val = circleNoiseGen(now + 1000, theta);
264 if (val < threshold) {
265 val = zero;
266 } else {
267 val = mapf(val, threshold, max_val, zero, max_val);
268 }
269 u8 val_u8 = u8(val.to_int());
270 // Palette lookup: val_u8 selects color AND brightness
271 dst[i] = ColorFromPalette(mNoisePalette, val_u8, val_u8, LINEARBLEND);
272 theta = theta + step;
273 }
274}
275
276// ---------------------------------------------------------------------------
277// Rendering helpers
278// ---------------------------------------------------------------------------
279
281 if (src.r > dst.r)
282 dst.r = src.r;
283 if (src.g > dst.g)
284 dst.g = src.g;
285 if (src.b > dst.b)
286 dst.b = src.b;
287}
288
290 int center = int(p.position);
291 float frac = p.position - float(center);
292 u8 bri = clamp_u8(p.brightness);
293 CRGB baseColor =
294 ColorFromPalette(mAmbientPalette, p.paletteIndex, bri, LINEARBLEND);
295
296 for (int offset = 0; offset < p.headWidth; ++offset) {
297 int idx = center - offset; // trail extends behind (toward pos 0)
298 if (idx < 0 || idx >= mNumLeds)
299 continue;
300
301 // Linear falloff: 100% at head → 20% at tail of gradient
302 float falloff = 1.0f - (float(offset) / float(p.headWidth)) * 0.8f;
303 CRGB pixel = baseColor;
304 pixel.nscale8(clamp_u8(falloff * 255.0f));
305
306 // Sub-pixel blending for the leading edge
307 if (offset == 0 && frac > 0.01f) {
308 int nextIdx = center + 1;
309 if (nextIdx < mNumLeds) {
310 CRGB subPixel = pixel;
311 subPixel.nscale8(clamp_u8(frac * 255.0f));
312 writeMax(mTrailBuffer[nextIdx], subPixel);
313 }
314 pixel.nscale8(clamp_u8((1.0f - frac) * 255.0f));
315 }
316
317 writeMax(mTrailBuffer[idx], pixel);
318 }
319}
320
322 int center = int(m.position);
323
324 // --- Head: 5-pixel gaussian kernel ---
325 static const float kGaussian[] = {0.15f, 0.60f, 1.0f, 0.60f, 0.15f};
326 for (int offset = -2; offset <= 2; ++offset) {
327 int idx = center + offset;
328 if (idx < 0 || idx >= mNumLeds)
329 continue;
330 float weight = kGaussian[offset + 2];
331 u8 bri = clamp_u8(255.0f * m.intensity * weight);
332 // Re-entry sparkle: random brightness jitter per pixel per frame
333 bri = scale8(bri, random8(153, 255));
334 CRGB headColor = mMeteorHeadColor;
335 headColor.nscale8(bri);
336 writeMax(mTrailBuffer[idx], headColor);
337 }
338
339 // --- Tail: gradient behind the head ---
340 float tailLen = m.tailLength();
341 int tailPixels = int(tailLen);
342 for (int i = 1; i <= tailPixels; ++i) {
343 int idx = center - i;
344 if (idx < 0)
345 break;
346 if (idx >= mNumLeds)
347 continue;
348
349 // t: 0.0 at head, 1.0 at tail tip
350 float t = float(i) / float(tailPixels);
351 fract8 blendAmt = clamp_u8(t * 255.0f);
352 CRGB tailColor = blend(mMeteorMidColor, mMeteorTailColor, blendAmt);
353 // Quadratic brightness falloff along tail
354 float bri = (1.0f - t * t) * m.intensity;
355 tailColor.nscale8(clamp_u8(bri * 255.0f));
356 writeMax(mTrailBuffer[idx], tailColor);
357 }
358}
359
361 int idx = int(d.position);
362 float frac = d.position - float(idx);
363 u8 bri = clamp_u8(d.brightness);
364 CRGB pixel = d.color;
365 pixel.nscale8(bri);
366
367 if (idx >= 0 && idx < mNumLeds) {
368 CRGB main = pixel;
369 main.nscale8(clamp_u8((1.0f - frac) * 255.0f));
370 writeMax(mTrailBuffer[idx], main);
371 }
372 int nextIdx = idx + 1;
373 if (nextIdx >= 0 && nextIdx < mNumLeds && frac > 0.01f) {
374 CRGB sub = pixel;
375 sub.nscale8(clamp_u8(frac * 255.0f));
376 writeMax(mTrailBuffer[nextIdx], sub);
377 }
378}
379
382 if (!d)
383 return;
384
385 float tailLen = m.tailLength();
386 u8 maxOffset = clamp_u8(tailLen);
387 if (maxOffset < 3)
388 maxOffset = 3;
389 float spawnOffset = float(random8(2, maxOffset));
390 float spawnPos = m.position - spawnOffset;
391 if (spawnPos < 0.0f)
392 spawnPos = 0.0f;
393
394 // Interpolate meteor tail color at detach point
395 float t = spawnOffset / tailLen;
396 if (t > 1.0f)
397 t = 1.0f;
398 fract8 blendAmt = clamp_u8(t * 255.0f);
399 CRGB debrisColor = blend(mMeteorMidColor, mMeteorTailColor, blendAmt);
400
401 d->alive = true;
402 d->position = spawnPos;
403 d->velocity = float(random8(10, 40)) / 100.0f; // 0.1-0.4 LEDs/frame
404 d->brightness = 180.0f;
405 d->color = debrisColor;
406 m.debrisSpawned++;
407}
408
409// ---------------------------------------------------------------------------
410// Main draw
411// ---------------------------------------------------------------------------
412
414 fl::span<CRGB> leds = context.leds;
415 if (leds.empty() || mNumLeds == 0) {
416 return;
417 }
418 u32 now = context.now;
419
420 // --- Layer 1: Perlin noise background (fresh each frame) ---
421 noiseCircleDraw(now, leds);
422
423 // --- Trail buffer decay ---
424 // Use the larger of the two trail intensities for the shared buffer.
425 // Higher intensity value = less fade per frame = longer trails.
429 u8 decayRate = 255 - trailIntensity;
430 fadeToBlackBy(mTrailBuffer.data(), mNumLeds, decayRate);
431
432 // --- Layer 2: Ambient particles ---
433 for (u16 i = 0; i < (u16)mAmbientParticles.size(); ++i) {
435 if (!p.alive)
436 continue;
437 // Physics update
438 p.velocity *= mDrag;
439 p.position += p.velocity;
441 if (p.position >= float(mNumLeds) || p.position < 0.0f ||
442 p.brightness < 8.0f || p.velocity < mMinVelocity) {
443 p.alive = false;
444 continue;
445 }
446 renderAmbient(p);
447 }
448
449 // --- Layer 3: Meteors ---
450 for (u16 i = 0; i < (u16)mMeteorParticles.size(); ++i) {
452 if (!m.alive)
453 continue;
454 // Debris spawn check before physics update
455 if (m.shouldSpawnDebris()) {
456 spawnDebrisFromMeteor(m, now);
457 }
458 // Physics update
460 m.position += m.velocity;
461 m.frameCounter++;
462 if (m.position >= float(mNumLeds) || m.velocity < mMinVelocity) {
463 m.alive = false;
464 continue;
465 }
466 renderMeteor(m);
467 }
468
469 // --- Debris ---
470 for (u16 i = 0; i < (u16)mDebrisParticles.size(); ++i) {
472 if (!d.alive)
473 continue;
474 // Physics update
475 d.position += d.velocity;
478 if (d.brightness < 5.0f || d.position >= float(mNumLeds)) {
479 d.alive = false;
480 continue;
481 }
482 renderDebris(d);
483 }
484
485 // --- Composite: max(noise, trail) per channel ---
486 for (u16 i = 0; i < mNumLeds; ++i) {
487 if (mTrailBuffer[i].r > leds[i].r)
488 leds[i].r = mTrailBuffer[i].r;
489 if (mTrailBuffer[i].g > leds[i].g)
490 leds[i].g = mTrailBuffer[i].g;
491 if (mTrailBuffer[i].b > leds[i].b)
492 leds[i].b = mTrailBuffer[i].b;
493 }
494}
495
496} // namespace fl
fl::CRGB leds[NUM_LEDS]
uint32_t z[NUM_LAYERS]
Definition Fire2023.h:93
UINumberField palette("Palette", 0, 0, 2)
uint16_t speed
Definition Noise.ino:66
Fx1d(u16 numLeds)
Definition fx1d.h:12
u16 mNumLeds
Definition fx.h:53
void spawnMeteor(float intensity=1.0f)
Spawn a BEAT meteor at position 0, traveling toward end of strip.
void setMeteorGradient(CRGB headColor, CRGB midColor, CRGB tailColor)
Set the meteor color gradient: head → mid → tail.
fl::vector< DebrisParticle > mDebrisParticles
void setAmbientTrailIntensity(u8 intensity)
Ambient trail intensity: 0 = no trail, 255 = long persistent trail.
fl::vector< MeteorParticle > mMeteorParticles
void spawnDebrisFromMeteor(MeteorParticle &m, u32 now)
void setAmbientBrightnessDecay(float decay)
Per-frame brightness decay for ambient particles.
void setDebrisBrightnessDecay(float decay)
Per-frame brightness decay for debris particles. Default 0.90.
void noiseCircleDraw(u32 now, fl::span< CRGB > dst)
void renderMeteor(const MeteorParticle &m)
void setDebrisVelocityDecay(float decay)
Per-frame velocity decay for debris particles. Default 0.95.
void draw(DrawContext context) override
static s16x16 mapf(s16x16 x, s16x16 in_min, s16x16 in_max, s16x16 out_min, s16x16 out_max)
s16x16 circleNoiseGen(u32 now, s16x16 theta) const
static void writeMax(CRGB &dst, const CRGB &src)
void setNoisePalette(const CRGBPalette16 &palette)
Set palette for Perlin noise background.
void setMinVelocity(float minVel)
Minimum velocity before a particle dies.
~PerlinParticlePunch() FL_NOEXCEPT
void setAmbientPalette(const CRGBPalette16 &palette)
Set palette for ambient particles.
void setDrag(float drag)
Set per-frame drag for ambient particles (0.0 = instant stop, 1.0 = no drag).
void setSpeed(float speed)
Set velocity multiplier. Default 1.0.
void renderDebris(const DebrisParticle &d)
void renderAmbient(const AmbientParticle &p)
void setTimeMultiplier(float mult)
Set time multiplier for noise evolution (1.0 = normal, >1 = warp).
fl::string fxName() const override
void spawnAmbient(float intensity=0.5f)
Spawn an ambient particle at a random position.
void setMeteorTrailIntensity(u8 intensity)
Meteor trail intensity: controls how long meteor/debris trails linger.
fl::vector< AmbientParticle > mAmbientParticles
static constexpr i32 SCALE
Definition s16x16.h:23
static FASTLED_FORCE_INLINE void sincos(s16x16 angle, s16x16 &out_sin, s16x16 &out_cos) FL_NOEXCEPT
Definition s16x16.h:311
constexpr i32 to_int() const FL_NOEXCEPT
Definition s16x16.h:61
static constexpr int FRAC_BITS
Definition s16x16.h:22
static constexpr FASTLED_FORCE_INLINE s16x16 from_raw(i32 raw) FL_NOEXCEPT
Definition s16x16.h:54
constexpr i32 raw() const FL_NOEXCEPT
Definition s16x16.h:60
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
fl::u16 inoise16(fl::u32 x, fl::u32 y, fl::u32 z, fl::u32 t)
LIB8STATIC fl::u8 random8() FL_NOEXCEPT
Generate an 8-bit random number.
Definition random8.h:53
u8 fract8
Fixed-Point Fractional Types.
Definition s16x16x4.h:161
unsigned char u8
Definition stdint.h:131
fl::CRGB CRGB
Definition video.h:15
void fadeToBlackBy(CRGB *leds, fl::u16 num_leds, fl::u8 fadeBy)
CRGB ColorFromPalette(const CRGBPalette16 &pal, fl::u8 index, fl::u8 brightness, TBlendType blendType)
fl::i64 i64
Definition s16x16x4.h:222
CRGB blend(const CRGB &p1, const CRGB &p2, fract8 amountOfP2)
constexpr enable_if< is_fixed_point< T >::value, T >::type step(T edge, T x) FL_NOEXCEPT
Base definition for an LED controller.
Definition crgb.hpp:179
Audio-reactive perlin noise background with ambient particles and beat meteor overlay.
#define FL_NOEXCEPT
CRGB & nscale8(u8 scaledown) FL_NOEXCEPT
Scale down a RGB to N/256ths of its current brightness, using "plain math" dimming rules.
Definition crgb.cpp.hpp:88
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:510
Representation of an 8-bit RGB pixel (Red, Green, Blue)
Definition crgb.h:38
fl::span< CRGB > leds