Lamp-Da 0.1
A compact lantern project
Loading...
Searching...
No Matches
fadeout.hpp
Go to the documentation of this file.
1
5#ifndef MODES_ANIM_FADEOUT_HPP
6#define MODES_ANIM_FADEOUT_HPP
7
9
10namespace lampda::modes::anims {
12namespace fadeout {
13
23{
28 template<typename ContextType, uint8_t MaskBuffId, uint8_t minMaskValue>
29 static bool depop_one(ContextType& ctx, uint32_t& pixelId)
30 {
31 auto& buffer = ctx.lamp.template getTempBuffer<MaskBuffId>();
32 const auto bufferSize = ctx.lamp.ledCount;
33
34 // check lines by lines from bottom to top, keeping the first one with pixels left
35 for (int16_t y = ctx.lamp.maxHeight; y >= 0; y--)
36 {
37 // count pixels set in this line
38 uint8_t pixelSetCount = 0;
39 for (uint16_t x = 0; x <= ctx.lamp.maxWidth; x++)
40 {
41 const auto buffIndex = modes::to_strip(x, y);
42 if (buffIndex < 0 or buffIndex >= bufferSize)
43 continue;
44 if (buffer[buffIndex] > minMaskValue)
45 pixelSetCount += 1;
46 }
47
48 // found the first line with pixels left
49 if (pixelSetCount > 0)
50 {
51 // select random pixel in this line
52 uint8_t selectedLed = lmpd_map<uint8_t>(rand(), 0, RAND_MAX, 0, pixelSetCount);
53 for (uint16_t x = 0; x <= ctx.lamp.maxWidth; x++)
54 {
55 const auto tempPixelId = modes::to_strip(x, y);
56 if (tempPixelId < 0 or tempPixelId >= bufferSize)
57 continue;
58 if (buffer[tempPixelId] > minMaskValue)
59 {
60 if (selectedLed == 0)
61 {
62 pixelId = tempPixelId; // Return the pixel ID to depop
63 return true;
64 }
65 selectedLed -= 1;
66 }
67 }
68 break;
69 }
70 }
71 return false; // No pixel found
72 }
73};
74
80{
84 template<typename ContextType, uint8_t MaskBuffId, uint8_t minMaskValue>
85 static bool has_pixel_below(ContextType& ctx, uint16_t x, int16_t y)
86 {
87 auto& buffer = ctx.lamp.template getTempBuffer<MaskBuffId>();
88 const auto bufferSize = ctx.lamp.ledCount;
89
90 if (y >= ctx.lamp.maxHeight or (y + 1) > ctx.lamp.maxHeight)
91 {
92 return false;
93 }
94
95 // check the line below
96 const auto pixelId = modes::to_strip(x, y + 1);
97 if (pixelId >= 0 and pixelId < bufferSize)
98 {
99 if (buffer[pixelId] > minMaskValue)
100 return true; // Found a pixel below
101 }
102 // check offsetted line, lamp is not wrapped straight !
103 const auto pixelIdOffseted = modes::to_strip(x - 1, y + 1);
104 if (pixelIdOffseted >= 0 and pixelIdOffseted < bufferSize)
105 {
106 if (buffer[pixelIdOffseted] > minMaskValue)
107 return true; // Found a pixel below
108 }
109 return false;
110 }
111
112 template<typename ContextType, uint8_t MaskBuffId, uint8_t minMaskValue>
113 static bool depop_one(ContextType& ctx, uint32_t& pixelId)
114 {
115 auto& buffer = ctx.lamp.template getTempBuffer<MaskBuffId>();
116 const auto bufferSize = ctx.lamp.ledCount;
117 uint8_t orphanCount = 0;
118
119 int16_t minYLine = ctx.lamp.maxHeight;
120 int16_t maxYLine = 0;
121
122 // Count all orphan pixels
123 for (int16_t y = ctx.lamp.maxHeight; y >= 0; y--)
124 {
125 bool hasSetPixels = false;
126 bool hasOrphanPixels = false;
127 for (uint16_t x = 0; x <= ctx.lamp.maxWidth; x++)
128 {
129 const auto tempPixelId = modes::to_strip(x, y);
130 if (tempPixelId < 0 or tempPixelId >= bufferSize)
131 continue;
132 if (buffer[tempPixelId] > minMaskValue)
133 {
134 hasSetPixels = true;
135
136 // Check if this pixel is an orphan (no pixel below it)
137 if (not has_pixel_below<ContextType, MaskBuffId, minMaskValue>(ctx, x, y))
138 {
139 orphanCount += 1;
140 hasOrphanPixels = true;
141
142 minYLine = min<int16_t>(minYLine, y);
143 maxYLine = max<int16_t>(maxYLine, y);
144 }
145 }
146 }
147 // line with pixels and no orphans, we can quit the check here
148 if (hasSetPixels and not hasOrphanPixels)
149 {
150 break;
151 }
152 }
153
154 if (orphanCount <= 0)
155 {
156 bsp::lampda_print("Could not find orphan nodes, skipping");
157 return false;
158 }
159
160 uint8_t selectedLed = lmpd_map<uint8_t>(rand(), 0, RAND_MAX, 0, orphanCount);
161
162 // drop a random orphan pixels
163 for (int16_t y = maxYLine; y >= minYLine; y--)
164 {
165 for (uint16_t x = 0; x <= ctx.lamp.maxWidth; x++)
166 {
167 const auto tempPixelId = modes::to_strip(x, y);
168 if (tempPixelId < 0 or tempPixelId >= bufferSize)
169 continue;
170 if (buffer[tempPixelId] > minMaskValue)
171 {
172 // Check if this pixel is an orphan (no pixel below it)
173 if (not has_pixel_below<ContextType, MaskBuffId, minMaskValue>(ctx, x, y))
174 {
175 if (selectedLed == 0)
176 {
177 pixelId = tempPixelId; // Found the selected orphan
178 return true;
179 }
180 selectedLed -= 1;
181 }
182 }
183 }
184 }
185 return false; // No orphan pixel found
186 }
187};
188
195template<uint8_t MaskBuffId, typename DropMode = DropModeOrphansOnly, uint8_t minMaskValue = 0> struct GravityDissolve
196{
197 void reset(auto& ctx)
198 {
199 // set all values to max: no masking
200 ctx.lamp.template fillTempBuffer<MaskBuffId>(UINT8_MAX);
201 // reset particles
202 particuleSystem.reset();
203 particuleSystem.set_max_particle_count(20);
204
205 latestProgress = -1.0;
206 latestProgressTime = 0;
207 particlesToDepopPerIteration = 0.0;
208 depopRate = 0.0;
209 particlesDropped = 0;
210 }
211
212 void loop(auto& ctx, uint32_t color)
213 {
214 // find the particles to depop
215 depopRate += particlesToDepopPerIteration;
216 // time to depop one of more particle
217 float integer;
218 const float fractional = modf(depopRate, &integer);
219 if (integer >= 1.0 and particlesDropped < ctx.lamp.ledCount)
220 {
221 // time to depop !
222 for (uint8_t depopIndex = 0; depopIndex < integer; depopIndex++)
223 {
224 // depop one particle, yay !
225 depop_one_particles(ctx);
226 }
227
228 // drop depop rate
229 depopRate = fractional;
230 }
231
232 // no updates if no particles
233 if (particuleSystem.get_number_of_active() > 0)
234 {
235 // depop particles out of bounds
236 static constexpr bool shouldKeepInLampBounds = false;
237 particuleSystem.iterate_no_collisions(
238 utils::vec3d(0.0, 0.0, -9.81 / 4.0), ctx.lamp.frameDurationMs / 1000.0, shouldKeepInLampBounds);
239 particuleSystem.depop_particules(recycle_particules_if_too_far);
240 particuleSystem.show(
241 [&](int16_t n, const Particle& particle) {
242 return color;
243 },
244 ctx.lamp);
245 }
246 }
247
252 void update_depop_rate(auto& ctx, const float expectedProgress)
253 {
254 // Artifically lenghten the progress to let particles disappear
255 const float progress = expectedProgress / gracePeriod;
256
257 const float updateDuration = static_cast<float>((ctx.lamp.now - latestProgressTime) / 1000.0);
258 if (updateDuration <= 0.0f)
259 return;
260
261 const float progressPerSecond = (progress - latestProgress) / updateDuration;
262 bool isValidCall = latestProgress >= 0 and updateDuration > 0;
263
264 // cancel the particle spawn
265 if (progress <= 0.0)
266 {
267 particlesToDepopPerIteration = 0.0;
268 latestProgress = 0.0;
269 isValidCall = false;
270 }
271 // update progress
272 else if (latestProgress != progress)
273 {
274 latestProgress = progress;
275 latestProgressTime = ctx.lamp.now;
276 }
277 else
278 isValidCall = false;
279
280 // not valid call, skip update
281 if (not isValidCall)
282 {
283 return;
284 }
285
286 const uint16_t particleMax = ctx.lamp.ledCount;
287 const float frameFrequency = 1000.0 / ctx.lamp.frameDurationMs;
288
289 const float particlesLeft = particleMax - particlesDropped;
290 const float trueProgress = 1.0 - particlesLeft / static_cast<float>(particleMax);
291
292 // How many particles SHOULD have been dropped by now?
293 const float expectedParticlesDropped = particleMax * progress;
294 const float particleDeficit = expectedParticlesDropped - static_cast<float>(particlesDropped);
295
296 const float remainingProgressRatio = 1.0 - progress;
297
298 // SMOOTH CATCH-UP: Distribute deficit + normal depop over remaining progress window
299 // This ensures we catch up gradually, not all at once
300 if (remainingProgressRatio > 0.0f && particlesLeft > 0.0f)
301 {
302 // Normal depop rate (if we were on track)
303 float normalDepopRate = progressPerSecond * particlesLeft / frameFrequency;
304
305 // Spread the deficit evenly over the remaining progress window
306 // deficit / (remaining_progress / progress_rate) = deficit / time_to_finish
307 float deficitCatchupRate = 0.0f;
308 if (progressPerSecond > 0.0f)
309 {
310 const float timeToFinish = remainingProgressRatio / progressPerSecond;
311 deficitCatchupRate = particleDeficit / timeToFinish / frameFrequency;
312 }
313
314 // Combine: normal rate + deficit catch-up spread over remaining time
315 particlesToDepopPerIteration = normalDepopRate + deficitCatchupRate;
316 }
317 else if (particlesLeft > 0.0f && progress >= 1.0f)
318 {
319 // Progress reached 1.0 but particles remain; depop everything immediately
320 particlesToDepopPerIteration = particlesLeft;
321 }
322 else
323 {
324 particlesToDepopPerIteration = 0.0;
325 }
326
327 // Safeguard: cap depop rate to prevent overshoot
328 const float maxParticlesToDepop = particlesLeft;
329 if (particlesToDepopPerIteration > maxParticlesToDepop)
330 particlesToDepopPerIteration = maxParticlesToDepop;
331 }
332
333protected:
334 static bool recycle_particules_if_too_far(const Particle& p)
335 {
336 static constexpr float bounds = 3.0;
337 return p.z_mm > LampTy::maxWidth * bounds or p.z_mm < -(LampTy::lampHeight_mm + LampTy::maxWidth * bounds);
338 }
339
340 bool depop_one_particles(auto& ctx)
341 {
342 auto& buffer = ctx.lamp.template getTempBuffer<MaskBuffId>();
343 const auto bufferSize = ctx.lamp.ledCount;
344
345 // Select a pixel to drop
346 uint32_t pixelId;
347 if (not DropMode::template depop_one<decltype(ctx), MaskBuffId, minMaskValue>(ctx, pixelId) or pixelId < 0 or
348 pixelId >= bufferSize)
349 {
350 bsp::lampda_print("Error: Invalid pixel ID from drop mode");
351 return false;
352 }
353
354 // drop this pixel
355 buffer[pixelId] = minMaskValue;
356 particlesDropped += 1;
357
358 // spawn a new particle
359 const bool isCreated = particuleSystem.init_deferred_particules(1, [pixelId](size_t) {
360 return pixelId;
361 });
362 // Particle spawn can fail, it's only visual so ok
363
364 return true;
365 }
366
367private:
368 static constexpr float gracePeriod = 0.99;
369 //
370 float latestProgress = -1.0;
371 uint32_t latestProgressTime = 0;
372
373 float particlesToDepopPerIteration = 0.0;
374 float depopRate = 0.0;
375 size_t particlesDropped = 0;
376
378 inline static auto particuleSystem = modes::get_shared_particle_system();
379};
380
381} // namespace fadeout
382} // namespace lampda::modes::anims
383
384#endif
void lampda_print(const char *format,...)
C linkage to print functions.
Definition: text_out.cpp:227
static constexpr uint16_t to_strip(uint16_t, uint16_t)
convert grid coordinates to strip index
Definition: lamp_type.hpp:1104
static ParticleSystem & get_shared_particle_system()
Access the main level shared particle system.
Definition: particle_system.hpp:261
Define a particle system, with particle based logic.
Define a particle in 3D space. it has a position and speed.
Definition: particle.hpp:37
float z_mm
height, in millimeters.
Definition: particle.hpp:204
Drop mode policy: remove particles line by line from bottom to top Only pixels from the lowest non-em...
Definition: fadeout.hpp:23
static bool depop_one(ContextType &ctx, uint32_t &pixelId)
Find and depop one particle according to this mode.
Definition: fadeout.hpp:29
Drop mode policy: remove only "orphan" particles A particle can only be dropped if there is no other ...
Definition: fadeout.hpp:80
static bool has_pixel_below(ContextType &ctx, uint16_t x, int16_t y)
Check if a pixel at (x, y) has a pixel below it.
Definition: fadeout.hpp:85
Make an animation disapear with gravity.
Definition: fadeout.hpp:196
void update_depop_rate(auto &ctx, const float expectedProgress)
Update the drop rate.
Definition: fadeout.hpp:252
static constexpr float lampHeight_mm
Computation of the lamp height.
Definition: lamp_type.hpp:441
static constexpr uint16_t maxWidth
(indexable) Width of "pixel space" w/ lamp taken as a LED matrix
Definition: lamp_type.hpp:370
3d vector in any space
Definition: vector_math.h:54