Skip to content

Commit 74dd3c9

Browse files
committed
introduce strand for scorpio
1 parent 6885440 commit 74dd3c9

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

scorpio/strand/strand.ino

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/*
2+
Single-strand NeoPXL8 demo (pulses + twinkles)
3+
- Pulses: NO OVERLAP by position rule; spawn next after newest rear edge
4+
passes a random distance from the start (min MIN_PULSE_GAP_PX)
5+
- Smooth rendering: float accumulation + gamma + light temporal dithering
6+
- Twinkles: white channel, fixed rate; relocate each cycle, slow rise/fall
7+
*/
8+
9+
#include <Adafruit_NeoPXL8.h>
10+
#include <math.h>
11+
#include <vector>
12+
13+
// ======== USER TUNABLES ========
14+
#define STRAND_LEN 144
15+
#define COLOR_ORDER NEO_GRBW
16+
#define DATA_PIN 16
17+
18+
// Pulse motion
19+
const float PULSE_SPEED = 4.0f; // px/sec
20+
21+
// Pulse size (HALF-length randomized); visible total ≈ 2*halfLen
22+
const float PULSE_HALFLEN_MIN_PX = 1.0f;
23+
const float PULSE_HALFLEN_MAX_PX = 7.0f;
24+
25+
// Spacing rule: spawn next pulse when the newest pulse's REAR edge (leftmost)
26+
// has traveled at least: MIN_PULSE_GAP_PX + random(0..EXTRA_GAP_SPREAD_PX)
27+
const float MIN_PULSE_GAP_PX = 1.0f;
28+
const float EXTRA_GAP_SPREAD_PX = 18.0f; // randomness range; set 0 for fixed spacing
29+
30+
// Tail shaping — VERY SOFT EDGES
31+
// Brightness = (0.5 * (1 + cos(pi * (d / (halfLen * EDGE_SOFTNESS))))) ^ GAMMA
32+
const float EDGE_SOFTNESS = 2.0f; // extend tails
33+
const float PULSE_SHAPE_GAMMA = 0.35f; // <1 = gentler edges
34+
35+
// Temporal fade-in (attack) and fade-out (release)
36+
const uint16_t ATTACK_MS = 1500; // slow ramp-up
37+
const uint16_t RELEASE_MS = 2000; // slow fade-out at the end
38+
const float PRE_ENTRY_MARGIN_PX = 6.0f; // spawn further off-strip
39+
40+
// Peak per-pulse (pre-gamma)
41+
const uint8_t PULSE_PEAK_MAX = 160;
42+
43+
// Pulse color (same for all pulses)
44+
const uint8_t PULSE_R = 255, PULSE_G = 0, PULSE_B = 0;
45+
46+
// Twinkles
47+
const uint8_t TWINKLE_COUNT = 10;
48+
const uint8_t TWINKLE_MAX_WHITE = 80;
49+
const uint32_t TWINKLE_PERIOD_MS = 2400;
50+
51+
// ======== WIRING ========
52+
int8_t pins[8] = {DATA_PIN, -1, -1, -1, -1, -1, -1, -1};
53+
Adafruit_NeoPXL8 leds(STRAND_LEN, pins, COLOR_ORDER);
54+
55+
// ======== STATE ========
56+
struct Pulse
57+
{
58+
float center; // center position (px)
59+
float halfLen; // base half-length (px)
60+
float drawHalf; // half-length used for drawing = halfLen * EDGE_SOFTNESS
61+
uint32_t bornMs; // for attack envelope
62+
uint32_t endAtMs; // when rear edge has left the strip (for release window)
63+
};
64+
65+
std::vector<Pulse> pulses; // active pulses (dynamic)
66+
float nextSpawnRearThreshold = 0.0f; // absolute rear-edge distance target from pixel 0
67+
68+
// float accumulators
69+
float accR[STRAND_LEN], accG[STRAND_LEN], accB[STRAND_LEN], accW[STRAND_LEN];
70+
71+
// timing
72+
uint32_t lastFrameMs = 0;
73+
uint32_t frameCounter = 0; // for dithering phase
74+
75+
// --- Twinkles ---
76+
struct Twinkle
77+
{
78+
uint16_t pixel;
79+
int32_t phaseMs;
80+
float prevPhase;
81+
};
82+
Twinkle twinkles[TWINKLE_COUNT];
83+
84+
// ======== HELPERS ========
85+
static inline uint32_t packColor(uint8_t r, uint8_t g, uint8_t b, uint8_t w)
86+
{
87+
return leds.Color(r, g, b, w);
88+
}
89+
static inline float frand(float a, float b)
90+
{
91+
return a + (b - a) * (float)random(0, 10000) / 10000.0f;
92+
}
93+
static inline float randGap()
94+
{
95+
return MIN_PULSE_GAP_PX + frand(0.0f, EXTRA_GAP_SPREAD_PX);
96+
}
97+
// simple clamp 0..1
98+
static inline float clamp01(float x) { return x < 0 ? 0 : (x > 1 ? 1 : x); }
99+
// cosine ease 0..1 -> 0..1
100+
static inline float easeCos(float u)
101+
{
102+
if (u <= 0)
103+
return 0.0f;
104+
if (u >= 1)
105+
return 1.0f;
106+
return 0.5f * (1.0f - cosf((float)M_PI * u));
107+
}
108+
// pseudo-random in [0,1) (for dithering), takes full 32-bit frame counter
109+
static inline float hash01(uint16_t x, uint32_t y)
110+
{
111+
uint32_t h = 2166136261u;
112+
h = (h ^ x) * 16777619u;
113+
h = (h ^ (uint16_t)(y)) * 16777619u;
114+
h = (h ^ (uint16_t)(y >> 16)) * 16777619u;
115+
return (h & 0xFFFFFF) / 16777216.0f;
116+
}
117+
118+
// Spawn a new pulse off-strip (to the left)
119+
void spawnPulse()
120+
{
121+
Pulse p;
122+
p.halfLen = frand(PULSE_HALFLEN_MIN_PX, PULSE_HALFLEN_MAX_PX);
123+
p.drawHalf = p.halfLen * EDGE_SOFTNESS;
124+
p.center = -(p.drawHalf + PRE_ENTRY_MARGIN_PX); // start very faint
125+
p.bornMs = millis();
126+
127+
// travel time until rear edge exits the strip (center - drawHalf > STRAND_LEN)
128+
float travel = (STRAND_LEN + p.drawHalf + PRE_ENTRY_MARGIN_PX + 12.0f) / fmaxf(PULSE_SPEED, 1.0f);
129+
p.endAtMs = p.bornMs + (uint32_t)(travel * 1000.0f);
130+
131+
pulses.push_back(p);
132+
133+
// Set next rear-edge threshold (absolute distance from start)
134+
float newestRear = fmaxf(0.0f, p.center - p.drawHalf); // negative at spawn -> 0
135+
nextSpawnRearThreshold = newestRear + randGap();
136+
}
137+
138+
void initTwinkles()
139+
{
140+
for (uint8_t i = 0; i < TWINKLE_COUNT; i++)
141+
{
142+
twinkles[i].pixel = random(0, STRAND_LEN);
143+
twinkles[i].phaseMs = random(0, TWINKLE_PERIOD_MS);
144+
twinkles[i].prevPhase = 0.0f;
145+
}
146+
}
147+
static inline void relocateTwinkleToStart(Twinkle &t, uint32_t nowMs)
148+
{
149+
t.pixel = random(0, STRAND_LEN);
150+
t.phaseMs = -(int32_t)(nowMs % TWINKLE_PERIOD_MS);
151+
t.prevPhase = 0.0f;
152+
}
153+
154+
void renderFrame(float dt)
155+
{
156+
// clear accumulators
157+
for (int i = 0; i < STRAND_LEN; i++)
158+
accR[i] = accG[i] = accB[i] = accW[i] = 0.0f;
159+
160+
const float peakR = (PULSE_R / 255.0f) * (PULSE_PEAK_MAX / 255.0f);
161+
const float peakG = (PULSE_G / 255.0f) * (PULSE_PEAK_MAX / 255.0f);
162+
const float peakB = (PULSE_B / 255.0f) * (PULSE_PEAK_MAX / 255.0f);
163+
164+
uint32_t nowMs = millis();
165+
166+
// Advance & draw pulses; remove those that have fully passed
167+
for (size_t i = 0; i < pulses.size(); /* increment inside */)
168+
{
169+
Pulse &p = pulses[i];
170+
171+
// move
172+
p.center += PULSE_SPEED * dt;
173+
174+
// cull if done (rear edge beyond strip)
175+
if (nowMs > p.endAtMs || (p.center - p.drawHalf) > (STRAND_LEN + 1))
176+
{
177+
pulses.erase(pulses.begin() + i);
178+
continue;
179+
}
180+
181+
// Temporal envelopes:
182+
// Attack : 0→1 over ATTACK_MS from bornMs
183+
float attack = easeCos((float)(nowMs - p.bornMs) / (float)ATTACK_MS);
184+
185+
// ✅ Correct release: 1→0 over the *last* RELEASE_MS before endAtMs
186+
int32_t tToEnd = (int32_t)p.endAtMs - (int32_t)nowMs; // ms until end
187+
float u = clamp01((float)tToEnd / (float)RELEASE_MS); // 1..0 over final window
188+
float release = easeCos(u); // high until close to the end, then eases down
189+
190+
float env = attack * release;
191+
192+
// Draw pulse with very soft spatial falloff
193+
int start = (int)floorf(p.center - p.drawHalf);
194+
int end = (int)ceilf(p.center + p.drawHalf);
195+
if (start < 0)
196+
start = 0;
197+
if (end > STRAND_LEN)
198+
end = STRAND_LEN;
199+
200+
for (int px = start; px < end; px++)
201+
{
202+
float d = fabsf((float)px - p.center); // 0 at center → drawHalf at edges
203+
float x = d / p.drawHalf; // 0..1
204+
if (x > 1.0f)
205+
continue;
206+
207+
// Extremely soft raised-cosine with gamma < 1
208+
float bSpatial = powf(0.5f * (1.0f + cosf((float)M_PI * x)), PULSE_SHAPE_GAMMA);
209+
float b = env * bSpatial;
210+
211+
accR[px] += peakR * b;
212+
accG[px] += peakG * b;
213+
accB[px] += peakB * b;
214+
}
215+
216+
++i;
217+
}
218+
219+
// Spawn rule: first pulse immediately; then by rear-edge distance
220+
if (pulses.empty())
221+
{
222+
spawnPulse();
223+
}
224+
else
225+
{
226+
const Pulse &newest = pulses.back();
227+
float newestRear = newest.center - newest.drawHalf; // rear (left) edge
228+
float traveledFromStart = fmaxf(0.0f, newestRear);
229+
if (traveledFromStart >= nextSpawnRearThreshold)
230+
{
231+
spawnPulse();
232+
}
233+
}
234+
235+
// Twinkles
236+
for (uint8_t i = 0; i < TWINKLE_COUNT; i++)
237+
{
238+
Twinkle &t = twinkles[i];
239+
float phase = (float)((int32_t)(nowMs + t.phaseMs) % (int32_t)TWINKLE_PERIOD_MS) / (float)TWINKLE_PERIOD_MS;
240+
if (t.prevPhase > 0.9f && phase < 0.1f)
241+
{
242+
relocateTwinkleToStart(t, nowMs);
243+
phase = 0.0f;
244+
}
245+
t.prevPhase = phase;
246+
247+
float s = 0.5f * (1.0f - cosf(phase * 2.0f * (float)M_PI)); // 0..1
248+
float w = (s * TWINKLE_MAX_WHITE) / 255.0f;
249+
250+
int px = t.pixel;
251+
if (px >= 0 && px < STRAND_LEN)
252+
accW[px] = fmaxf(accW[px], w);
253+
}
254+
255+
// Quantize once, with gamma + light temporal dithering (long period)
256+
leds.clear();
257+
for (int px = 0; px < STRAND_LEN; px++)
258+
{
259+
float r = fminf(accR[px], 1.0f);
260+
float g = fminf(accG[px], 1.0f);
261+
float b = fminf(accB[px], 1.0f);
262+
float w = fminf(accW[px], 1.0f);
263+
264+
float thr = hash01(px, frameCounter); // 0..1
265+
266+
float r255 = r * 255.0f;
267+
uint8_t r8 = (uint8_t)floorf(r255 + ((r255 - floorf(r255)) > thr ? 1.0f : 0.0f));
268+
float g255 = g * 255.0f;
269+
uint8_t g8 = (uint8_t)floorf(g255 + ((g255 - floorf(g255)) > thr ? 1.0f : 0.0f));
270+
float b255 = b * 255.0f;
271+
uint8_t b8 = (uint8_t)floorf(b255 + ((b255 - floorf(b255)) > thr ? 1.0f : 0.0f));
272+
float w255 = w * 255.0f;
273+
uint8_t w8 = (uint8_t)floorf(w255 + ((w255 - floorf(w255)) > thr ? 1.0f : 0.0f));
274+
275+
r8 = leds.gamma8(r8);
276+
g8 = leds.gamma8(g8);
277+
b8 = leds.gamma8(b8);
278+
w8 = leds.gamma8(w8);
279+
280+
leds.setPixelColor(px, packColor(r8, g8, b8, w8));
281+
}
282+
}
283+
284+
// ======== ARDUINO ========
285+
void setup()
286+
{
287+
randomSeed(analogRead(28));
288+
#ifdef PIN_NEOPIXEL_POWER
289+
pinMode(PIN_NEOPIXEL_POWER, OUTPUT);
290+
digitalWrite(PIN_NEOPIXEL_POWER, LOW);
291+
#endif
292+
293+
if (!leds.begin())
294+
{
295+
pinMode(LED_BUILTIN, OUTPUT);
296+
for (;;)
297+
digitalWrite(LED_BUILTIN, (millis() / 200) & 1);
298+
}
299+
leds.setBrightness(255);
300+
leds.clear();
301+
leds.show();
302+
303+
initTwinkles();
304+
pulses.clear();
305+
spawnPulse(); // start with one pulse
306+
lastFrameMs = millis();
307+
}
308+
309+
void loop()
310+
{
311+
uint32_t now = millis();
312+
float dt = (now - lastFrameMs) / 1000.0f;
313+
if (dt < 0.001f)
314+
dt = 0.001f;
315+
if (dt > 0.050f)
316+
dt = 0.050f;
317+
318+
renderFrame(dt);
319+
leds.show();
320+
321+
frameCounter++; // advance dithering phase
322+
lastFrameMs = now;
323+
delay(16); // ~60 FPS
324+
}

0 commit comments

Comments
 (0)