FastLED 3.10.6
Loading...
Searching...
No Matches
mood_painter.cpp
Go to the documentation of this file.
1// mood_painter.cpp - layer mix, trails, pulses.
2#include "mood_painter.h"
3
4#include "fl/math/math.h"
5
6namespace mood_ring {
7
8namespace {
9
10constexpr fl::u32 kPulseLifetimeMs = 700;
11constexpr float kPulseSigmaBeat = 0.035f;
12constexpr float kPulseSigmaDownbeat = 0.060f;
13constexpr float kPulseFadeIn = 0.08f; // fraction of life to reach full
14
15// Trails: long when calm, medium when flowing, crisp when grooving.
16constexpr float kKeepCalm = 0.85f;
17constexpr float kKeepFlow = 0.60f;
18constexpr float kKeepGroove = 0.35f;
19
20// Below this a trail channel is treated as dark, so a decaying tail ends
21// cleanly instead of lingering as denormals.
22constexpr float kTrailFloor = 1.0f / 1024.0f;
23
24// Hue drift: slow travel of the family so a long quiet stretch still moves.
25constexpr float kHueDriftTurnsPerSecCalm = 0.004f;
26
27} // namespace
28
30 if (mBufBreath.size() != n)
31 mBufBreath.assign(n, RGBf{});
32 if (mBufFlow.size() != n)
33 mBufFlow.assign(n, RGBf{});
34 if (mBufGroove.size() != n)
35 mBufGroove.assign(n, RGBf{});
36 if (mMix.size() != n)
37 mMix.assign(n, RGBf{});
38 if (mTrail.size() != n)
39 mTrail.assign(n, RGBf{});
40}
41
42void MoodPainter::draw(const RoomSense &s, fl::u32 nowMs, fl::span<CRGB> out) {
43 if (out.empty())
44 return;
45 ensureBuffers(out.size());
46
47 const float dtSec =
48 (mLastMs == 0) ? 0.016f : static_cast<float>(nowMs - mLastMs) * 0.001f;
49 mLastMs = nowMs;
50
51 // Hue drift only while calm; a busy room gets its motion from the layers.
52 mHueDrift =
53 wrapTurns(mHueDrift + kHueDriftTurnsPerSecCalm * s.calm * dtSec);
54 const MoodPalette pal = paletteFor(s, mHueDrift);
55
56 // Every layer renders every frame, even at zero weight, so its
57 // envelopes, grains and phase correction keep running. Skipping a layer
58 // would freeze a half-finished flash and replay it when the layer
59 // returned. The pixel loops are cheap at ring scale.
60 RingCanvas breath(mBufBreath);
61 RingCanvas flow(mBufFlow);
62 RingCanvas groove(mBufGroove);
63 breath.clear();
64 flow.clear();
65 groove.clear();
66 mBreath.render(s, pal, dtSec, breath);
67 mFlow.render(s, pal, dtSec, flow);
68 mGroove.render(s, pal, dtSec, groove);
69
70 mixLayers(s);
71
72 if (tuning.trails) {
73 float keep = tuning.trailOverride;
74 if (keep < 0.0f) {
75 keep = kKeepCalm * s.calm + kKeepFlow * s.flow +
76 kKeepGroove * s.groove;
77 }
78 applyTrails(keep);
79 } else {
80 // Drop history so re-enabling does not smear a stale frame.
81 for (fl::size i = 0; i < mTrail.size(); ++i)
82 mTrail[i] = RGBf{};
83 }
84
85 if (tuning.pulses) {
86 // Pulses belong to groove. Weight them by it so a beat that fires
87 // while groove is fading in arrives at the right strength.
88 if (s.events.downbeat) {
89 emitPulse(1.0f * s.groove, true, nowMs);
90 } else if (s.events.kick || s.events.beat) {
91 emitPulse(0.7f * s.groove, false, nowMs);
92 }
93 drawPulses(pal, nowMs);
94 }
95
96 // The one and only quantisation.
97 for (fl::size i = 0; i < out.size(); ++i)
98 out[i] = toCRGB(mMix[i]);
99}
100
102 const float wb = clampf(s.calm * tuning.breathGain, 0.0f, 1.0f);
103 const float wf = clampf(s.flow * tuning.flowGain, 0.0f, 1.0f);
104 const float wg = clampf(s.groove * tuning.grooveGain, 0.0f, 1.0f);
105 const fl::size n = mMix.size();
106 for (fl::size i = 0; i < n; ++i) {
107 const RGBf &b = mBufBreath[i];
108 const RGBf &f = mBufFlow[i];
109 const RGBf &g = mBufGroove[i];
110 RGBf &m = mMix[i];
111 m.r = b.r * wb + f.r * wf + g.r * wg;
112 m.g = b.g * wb + f.g * wf + g.g * wg;
113 m.b = b.b * wb + f.b * wf + g.b * wg;
114 }
115}
116
117void MoodPainter::applyTrails(float keep) {
118 // Persistence by per-channel max, not accumulation: unity gain by
119 // construction, so a static frame yields exactly itself and a keep of
120 // zero is a pure passthrough. In float, so a dim tail fades smoothly
121 // to nothing instead of stepping 3 -> 2 -> 1 -> 0.
122 keep = clampf(keep, 0.0f, 0.98f);
123 const fl::size n = mMix.size();
124 for (fl::size i = 0; i < n; ++i) {
125 RGBf &t = mTrail[i];
126 RGBf &live = mMix[i];
127 t.r *= keep;
128 t.g *= keep;
129 t.b *= keep;
130 if (t.r < kTrailFloor)
131 t.r = 0.0f;
132 if (t.g < kTrailFloor)
133 t.g = 0.0f;
134 if (t.b < kTrailFloor)
135 t.b = 0.0f;
136 if (live.r > t.r)
137 t.r = live.r;
138 if (live.g > t.g)
139 t.g = live.g;
140 if (live.b > t.b)
141 t.b = live.b;
142 live = t;
143 }
144}
145
146void MoodPainter::emitPulse(float strength, bool downbeat, fl::u32 nowMs) {
147 if (strength <= 0.02f)
148 return;
149 int slot = -1;
150 for (int i = 0; i < kMaxPulses; ++i) {
151 if (!mPulses[i].active) {
152 slot = i;
153 break;
154 }
155 }
156 if (slot < 0) {
157 // All busy: recycle the oldest. The newest hits are the ones the
158 // eye is tracking.
159 slot = 0;
160 for (int i = 1; i < kMaxPulses; ++i) {
161 if (mPulses[i].startMs < mPulses[slot].startMs)
162 slot = i;
163 }
164 }
165 mPulses[slot].startMs = nowMs;
166 mPulses[slot].strength = strength;
167 mPulses[slot].downbeat = downbeat;
168 mPulses[slot].active = true;
169}
170
171void MoodPainter::drawPulses(const MoodPalette &pal, fl::u32 nowMs) {
172 RingCanvas c(mMix);
173 const float origin = wrapTurns(tuning.pulseOrigin);
174 for (int i = 0; i < kMaxPulses; ++i) {
175 Pulse &p = mPulses[i];
176 if (!p.active)
177 continue;
178 const fl::u32 elapsed = nowMs - p.startMs;
179 if (elapsed >= kPulseLifetimeMs) {
180 p.active = false;
181 continue;
182 }
183 const float life =
184 static_cast<float>(elapsed) / static_cast<float>(kPulseLifetimeMs);
185 const float radius = life * 0.5f; // origin to the far side
186 const float sigma = p.downbeat ? kPulseSigmaDownbeat : kPulseSigmaBeat;
187 // Fade in over the first few percent so the ring grows out of the
188 // origin rather than appearing there, then dissolve as it travels.
189 const float amp =
190 p.strength * smoothstepf(0.0f, kPulseFadeIn, life) * (1.0f - life);
191 // Downbeats take the warm end of the family; beats stay white so the
192 // bar boundary is distinguishable by colour as well as by width.
193 const RGBf tint = p.downbeat ? paletteColor(pal, 1.0f) : rgbfWhite();
194 c.ring(origin, radius, sigma, tint, amp);
195 }
196}
197
199 int count = 0;
200 for (int i = 0; i < kMaxPulses; ++i) {
201 if (mPulses[i].active)
202 ++count;
203 }
204 return count;
205}
206
207} // namespace mood_ring
constexpr bool empty() const FL_NO_EXCEPT
Definition span.h:535
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
fl::vector< RGBf > mBufGroove
fl::vector< RGBf > mTrail
persistence shadow
fl::vector< RGBf > mBufFlow
void drawPulses(const MoodPalette &pal, fl::u32 nowMs)
void mixLayers(const RoomSense &s)
void applyTrails(float keep)
Pulse mPulses[kMaxPulses]
void ensureBuffers(fl::size n)
float mHueDrift
slow hue travel, turns of the wheel
fl::vector< RGBf > mBufBreath
static const int kMaxPulses
void emitPulse(float strength, bool downbeat, fl::u32 nowMs)
void draw(const RoomSense &s, fl::u32 nowMs, fl::span< CRGB > out)
Render one frame into out.
fl::vector< RGBf > mMix
this frame, summed
void ring(float origin, float radius, float sigma, const RGBf &color, float gain)
Add a band at distance radius (turns, 0..0.5) from origin, on both sides of it at once: an expanding ...
Additive drawing helpers over a span of float pixels.
Definition ring_canvas.h:66
static uint32_t t
Definition Luminova.h:55
float smoothstepf(float e0, float e1, float x)
0 below e0, 1 above e1, smooth in between.
CRGB toCRGB(const RGBf &p)
MoodPalette paletteFor(const RoomSense &s, float hueDrift)
Derive the family for this frame. Cheap; call every frame.
float clampf(float v, float lo, float hi)
Definition ring_canvas.h:19
RGBf rgbfWhite()
Definition ring_canvas.h:36
RGBf paletteColor(const MoodPalette &p, float t)
Colour at position t in [0, 1] along the family, at value v.
float wrapTurns(float t)
Wrap into [0, 1).
Everything a visual is allowed to react to.
Definition room_sense.h:31
Linear-light float pixel, nominal range [0, 1], may exceed 1 before clip.
Definition ring_canvas.h:24