imported from "final" folder

This commit is contained in:
2025-11-28 12:12:50 +01:00
parent f9288986cf
commit ff8e725b35
1061 changed files with 225150 additions and 96 deletions

View File

@@ -0,0 +1,607 @@
/// @file Chromancer.ino
/// @brief Hexagonal LED display visualization
/// @example Chromancer.ino
///
/// This sketch is fully compatible with the FastLED web compiler. To use it do the following:
/// 1. Install Fastled: `pip install fastled`
/// 2. cd into this examples page.
/// 3. Run the FastLED web compiler at root: `fastled`
/// 4. When the compiler is done a web page will open.
/*
Original Source: https://github.com/ZackFreedman/Chromance
GaryWoo's Video: https://www.youtube.com/watch?v=-nSCtxa2Kp0
GaryWoo's LedMap: https://gist.github.com/Garywoo/b6cd1ea90cb5e17cc60b01ae68a2b770
GaryWoo's presets: https://gist.github.com/Garywoo/82fa67c6e1f9529dc16a01dd97d05d58
Chromance wall hexagon source (emotion controlled w/ EmotiBit)
Partially cribbed from the DotStar example
I smooshed in the ESP32 BasicOTA sketch, too
(C) Voidstar Lab 2021
*/
#include "fl/sketch_macros.h"
#include "fl/warn.h"
#if !SKETCH_HAS_LOTS_OF_MEMORY
// Platform does not have enough memory
// Other platforms have weird issues. Will revisit this later.
#include <Arduino.h>
void setup() {
// Use Serial.println instead of FL_WARN to prevent optimization away
Serial.begin(115200);
Serial.println("Chromancer.ino: setup() - Platform has insufficient memory for full demo");
}
void loop() {
// Use Serial.println instead of FL_WARN to prevent optimization away
Serial.println("Chromancer.ino: loop() - Platform has insufficient memory for full demo");
delay(1000); // Prevent rapid printing
}
#else
#include <FastLED.h>
#include "fl/screenmap.h"
#include "fl/math_macros.h"
#include "fl/json.h"
#include "fl/ui.h"
#include "fl/map.h"
#include "fl/str.h"
#include "./screenmap.json.h"
#include "./mapping.h"
#include "./ripple.h"
#include "./detail.h"
using namespace fl;
enum {
BlackStrip = 0,
GreenStrip = 1,
RedStrip = 2,
BlueStrip = 3,
};
// Strips are different lengths because I am a dumb
constexpr int lengths[] = {
154, // Black strip
168, // Green strip
84, // Red strip
154 // Blue strip
};
// non emscripten uses separate arrays for each strip. Eventually emscripten
// should support this as well but right now we don't
CRGB leds0[lengths[BlackStrip]] = {};
CRGB leds1[lengths[GreenStrip]] = {};
CRGB leds2[lengths[RedStrip]] = {}; // Red
CRGB leds3[lengths[BlueStrip]] = {};
CRGB *leds[] = {leds0, leds1, leds2, leds3};
byte ledColors[40][14][3]; // LED buffer - each ripple writes to this, then we
// write this to the strips
//float decay = 0.97; // Multiply all LED's by this amount each tick to create
// fancy fading tails
UISlider sliderDecay("decay", .97f, .8, 1.0, .01);
// These ripples are endlessly reused so we don't need to do any memory
// management
#define numberOfRipples 30
Ripple ripples[numberOfRipples] = {
Ripple(0), Ripple(1), Ripple(2), Ripple(3), Ripple(4), Ripple(5),
Ripple(6), Ripple(7), Ripple(8), Ripple(9), Ripple(10), Ripple(11),
Ripple(12), Ripple(13), Ripple(14), Ripple(15), Ripple(16), Ripple(17),
Ripple(18), Ripple(19), Ripple(20), Ripple(21), Ripple(22), Ripple(23),
Ripple(24), Ripple(25), Ripple(26), Ripple(27), Ripple(28), Ripple(29),
};
// Biometric detection and interpretation
// IR (heartbeat) is used to fire outward ripples
float lastIrReading; // When our heart pumps, reflected IR drops sharply
float highestIrReading; // These vars let us detect this drop
unsigned long
lastHeartbeat; // Track last heartbeat so we can detect noise/disconnections
#define heartbeatLockout \
500 // Heartbeats that happen within this many milliseconds are ignored
#define heartbeatDelta 300 // Drop in reflected IR that constitutes a heartbeat
// Heartbeat color ripples are proportional to skin temperature
#define lowTemperature 33.0 // Resting temperature
#define highTemperature 37.0 // Really fired up
float lastKnownTemperature =
(lowTemperature + highTemperature) /
2.0; // Carries skin temperature from temperature callback to IR callback
// EDA code was too unreliable and was cut.
// TODO: Rebuild EDA code
// Gyroscope is used to reject data if you're moving too much
#define gyroAlpha 0.9 // Exponential smoothing constant
#define gyroThreshold \
300 // Minimum angular velocity total (X+Y+Z) that disqualifies readings
float gyroX, gyroY, gyroZ;
// If you don't have an EmotiBit or don't feel like wearing it, that's OK
// We'll fire automatic pulses
#define randomPulsesEnabled true // Fire random rainbow pulses from random nodes
#define cubePulsesEnabled true // Draw cubes at random nodes
UICheckbox starburstPulsesEnabled("Starburst Pulses", true);
UICheckbox simulatedBiometricsEnabled("Simulated Biometrics", true);
#define autoPulseTimeout \
5000 // If no heartbeat is received in this many ms, begin firing
// random/simulated pulses
#define randomPulseTime 2000 // Fire a random pulse every (this many) ms
unsigned long lastRandomPulse;
byte lastAutoPulseNode = 255;
byte numberOfAutoPulseTypes =
randomPulsesEnabled + cubePulsesEnabled + int(starburstPulsesEnabled);
byte currentAutoPulseType = 255;
#define autoPulseChangeTime 30000
unsigned long lastAutoPulseChange;
#define simulatedHeartbeatBaseTime \
600 // Fire a simulated heartbeat pulse after at least this many ms
#define simulatedHeartbeatVariance \
200 // Add random jitter to simulated heartbeat
#define simulatedEdaBaseTime 1000 // Same, but for inward EDA pulses
#define simulatedEdaVariance 10000
unsigned long nextSimulatedHeartbeat;
unsigned long nextSimulatedEda;
// Helper function to check if a node is on the border
bool isNodeOnBorder(byte node) {
for (int i = 0; i < numberOfBorderNodes; i++) {
if (node == borderNodes[i]) {
return true;
}
}
return false;
}
UITitle title("Chromancer");
UIDescription description("Take 6 seconds to boot up. Chromancer is a wall-mounted hexagonal LED display that originally reacted to biometric data from an EmotiBit sensor. It visualizes your heartbeat, skin temperature, and movement in real-time. Chromancer also has a few built-in effects that can be triggered with the push of a button. Enjoy!");
UICheckbox allWhite("All White", false);
UIButton simulatedHeartbeat("Simulated Heartbeat");
UIButton triggerStarburst("Trigger Starburst");
UIButton triggerRainbowCube("Rainbow Cube");
UIButton triggerBorderWave("Border Wave");
UIButton triggerSpiral("Spiral Wave");
bool wasHeartbeatClicked = false;
bool wasStarburstClicked = false;
bool wasRainbowCubeClicked = false;
bool wasBorderWaveClicked = false;
bool wasSpiralClicked = false;
// Group related UI elements using UIGroup template multi-argument constructor
UIGroup effectTriggers("Effect Triggers", simulatedHeartbeat, triggerStarburst, triggerRainbowCube, triggerBorderWave, triggerSpiral);
UIGroup automationControls("Automation", starburstPulsesEnabled, simulatedBiometricsEnabled);
UIGroup displayControls("Display", sliderDecay, allWhite);
void setup() {
Serial.begin(115200);
Serial.println("*** LET'S GOOOOO ***");
Serial.println("JSON SCREENMAP");
Serial.println(JSON_SCREEN_MAP);
fl::fl_map<fl::string, ScreenMap> segmentMaps;
ScreenMap::ParseJson(JSON_SCREEN_MAP, &segmentMaps);
printf("Parsed %d segment maps\n", int(segmentMaps.size()));
for (auto kv : segmentMaps) {
Serial.print(kv.first.c_str());
Serial.print(" ");
Serial.println(kv.second.getLength());
}
// ScreenMap screenmaps[4];
ScreenMap red, black, green, blue;
bool ok = true;
auto red_it = segmentMaps.find("red_segment");
ok = (red_it != segmentMaps.end()) && ok;
if (red_it != segmentMaps.end()) red = red_it->second;
auto black_it = segmentMaps.find("back_segment");
ok = (black_it != segmentMaps.end()) && ok;
if (black_it != segmentMaps.end()) black = black_it->second;
auto green_it = segmentMaps.find("green_segment");
ok = (green_it != segmentMaps.end()) && ok;
if (green_it != segmentMaps.end()) green = green_it->second;
auto blue_it = segmentMaps.find("blue_segment");
ok = (blue_it != segmentMaps.end()) && ok;
if (blue_it != segmentMaps.end()) blue = blue_it->second;
if (!ok) {
Serial.println("Failed to get all segment maps");
return;
}
CRGB* red_leds = leds[RedStrip];
CRGB* black_leds = leds[BlackStrip];
CRGB* green_leds = leds[GreenStrip];
CRGB* blue_leds = leds[BlueStrip];
FastLED.addLeds<WS2812, 2>(black_leds, lengths[BlackStrip]).setScreenMap(black);
FastLED.addLeds<WS2812, 3>(green_leds, lengths[GreenStrip]).setScreenMap(green);
FastLED.addLeds<WS2812, 1>(red_leds, lengths[RedStrip]).setScreenMap(red);
FastLED.addLeds<WS2812, 4>(blue_leds, lengths[BlueStrip]).setScreenMap(blue);
FastLED.show();
}
void loop() {
unsigned long benchmark = millis();
FL_UNUSED(benchmark);
// Fade all dots to create trails
for (int strip = 0; strip < 40; strip++) {
for (int led = 0; led < 14; led++) {
for (int i = 0; i < 3; i++) {
ledColors[strip][led][i] *= sliderDecay.value();
}
}
}
for (int i = 0; i < numberOfRipples; i++) {
ripples[i].advance(ledColors);
}
for (int segment = 0; segment < 40; segment++) {
for (int fromBottom = 0; fromBottom < 14; fromBottom++) {
int strip = ledAssignments[segment][0];
int led = round(fmap(fromBottom, 0, 13, ledAssignments[segment][2],
ledAssignments[segment][1]));
leds[strip][led] = CRGB(ledColors[segment][fromBottom][0],
ledColors[segment][fromBottom][1],
ledColors[segment][fromBottom][2]);
}
}
if (allWhite) {
// for all strips
for (int i = 0; i < 4; i++) {
for (int j = 0; j < lengths[i]; j++) {
leds[i][j] = CRGB::White;
}
}
}
FastLED.show();
// Check if buttons were clicked
wasHeartbeatClicked = bool(simulatedHeartbeat);
wasStarburstClicked = bool(triggerStarburst);
wasRainbowCubeClicked = bool(triggerRainbowCube);
wasBorderWaveClicked = bool(triggerBorderWave);
wasSpiralClicked = bool(triggerSpiral);
if (wasSpiralClicked) {
// Trigger spiral wave effect from center
unsigned int baseColor = random(0xFFFF);
byte centerNode = 15; // Center node
// Create 6 ripples in a spiral pattern
for (int i = 0; i < 6; i++) {
if (nodeConnections[centerNode][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
centerNode, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
0.3 + (i * 0.1), // Varying speeds creates spiral effect
2000,
i % 2 ? alwaysTurnsLeft : alwaysTurnsRight); // Alternating turn directions
break;
}
}
}
}
lastHeartbeat = millis();
}
if (wasBorderWaveClicked) {
// Trigger immediate border wave effect
unsigned int baseColor = random(0xFFFF);
// Start ripples from each border node in sequence
for (int i = 0; i < numberOfBorderNodes; i++) {
byte node = borderNodes[i];
// Find an inward direction
for (int dir = 0; dir < 6; dir++) {
if (nodeConnections[node][dir] >= 0 &&
!isNodeOnBorder(nodeConnections[node][dir])) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, dir,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / numberOfBorderNodes) * i,
255, 255),
.4, 2000, 0);
break;
}
}
break;
}
}
}
lastHeartbeat = millis();
}
if (wasRainbowCubeClicked) {
// Trigger immediate rainbow cube effect
int node = cubeNodes[random(numberOfCubeNodes)];
unsigned int baseColor = random(0xFFFF);
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.5, 2000, behavior);
break;
}
}
}
}
lastHeartbeat = millis();
}
if (wasStarburstClicked) {
// Trigger immediate starburst effect
unsigned int baseColor = random(0xFFFF);
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
starburstNode, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
lastHeartbeat = millis();
}
if (wasHeartbeatClicked) {
// Trigger immediate heartbeat effect
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(15, i, 0xEE1111,
float(random(100)) / 100.0 * .1 + .4, 1000, 0);
break;
}
}
}
lastHeartbeat = millis();
}
if (millis() - lastHeartbeat >= autoPulseTimeout) {
// When biometric data is unavailable, visualize at random
if (numberOfAutoPulseTypes &&
millis() - lastRandomPulse >= randomPulseTime) {
unsigned int baseColor = random(0xFFFF);
if (currentAutoPulseType == 255 ||
(numberOfAutoPulseTypes > 1 &&
millis() - lastAutoPulseChange >= autoPulseChangeTime)) {
byte possiblePulse = 255;
while (true) {
possiblePulse = random(3);
if (possiblePulse == currentAutoPulseType)
continue;
switch (possiblePulse) {
case 0:
if (!randomPulsesEnabled)
continue;
break;
case 1:
if (!cubePulsesEnabled)
continue;
break;
case 2:
if (!starburstPulsesEnabled)
continue;
break;
default:
continue;
}
currentAutoPulseType = possiblePulse;
lastAutoPulseChange = millis();
break;
}
}
switch (currentAutoPulseType) {
case 0: {
int node = 0;
bool foundStartingNode = false;
while (!foundStartingNode) {
node = random(25);
foundStartingNode = true;
for (int i = 0; i < numberOfBorderNodes; i++) {
// Don't fire a pulse on one of the outer nodes - it
// looks boring
if (node == borderNodes[i])
foundStartingNode = false;
}
if (node == lastAutoPulseNode)
foundStartingNode = false;
}
lastAutoPulseNode = node;
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
// strip0.ColorHSV(baseColor
// + (0xFFFF / 6) * i,
// 255, 255),
Adafruit_DotStar_ColorHSV(baseColor, 255,
255),
float(random(100)) / 100.0 * .2 + .5, 3000,
1);
break;
}
}
}
}
break;
}
case 1: {
int node = cubeNodes[random(numberOfCubeNodes)];
while (node == lastAutoPulseNode)
node = cubeNodes[random(numberOfCubeNodes)];
lastAutoPulseNode = node;
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
// strip0.ColorHSV(baseColor
// + (0xFFFF / 6) * i,
// 255, 255),
Adafruit_DotStar_ColorHSV(baseColor, 255,
255),
.5, 2000, behavior);
break;
}
}
}
}
break;
}
case 2: {
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
lastAutoPulseNode = starburstNode;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
starburstNode, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
break;
}
default:
break;
}
lastRandomPulse = millis();
}
if (simulatedBiometricsEnabled) {
// Simulated heartbeat
if (millis() >= nextSimulatedHeartbeat) {
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
15, i, 0xEE1111,
float(random(100)) / 100.0 * .1 + .4, 1000, 0);
break;
}
}
}
nextSimulatedHeartbeat = millis() + simulatedHeartbeatBaseTime +
random(simulatedHeartbeatVariance);
}
// Simulated EDA ripples
if (millis() >= nextSimulatedEda) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
byte targetNode =
borderNodes[random(numberOfBorderNodes)];
byte direction = 255;
while (direction == 255) {
direction = random(6);
if (nodeConnections[targetNode][direction] < 0)
direction = 255;
}
ripples[j].start(
targetNode, direction, 0x1111EE,
float(random(100)) / 100.0 * .5 + 2, 300, 2);
break;
}
}
}
nextSimulatedEda = millis() + simulatedEdaBaseTime +
random(simulatedEdaVariance);
}
}
}
// Serial.print("Benchmark: ");
// Serial.println(millis() - benchmark);
}
#endif // __AVR__

View File

@@ -0,0 +1,80 @@
#pragma once
#include "fl/stdint.h"
inline uint32_t Adafruit_DotStar_ColorHSV(uint16_t hue, uint8_t sat, uint8_t val) {
uint8_t r, g, b;
// Remap 0-65535 to 0-1529. Pure red is CENTERED on the 64K rollover;
// 0 is not the start of pure red, but the midpoint...a few values above
// zero and a few below 65536 all yield pure red (similarly, 32768 is the
// midpoint, not start, of pure cyan). The 8-bit RGB hexcone (256 values
// each for red, green, blue) really only allows for 1530 distinct hues
// (not 1536, more on that below), but the full unsigned 16-bit type was
// chosen for hue so that one's code can easily handle a contiguous color
// wheel by allowing hue to roll over in either direction.
hue = (hue * 1530L + 32768) / 65536;
// Because red is centered on the rollover point (the +32768 above,
// essentially a fixed-point +0.5), the above actually yields 0 to 1530,
// where 0 and 1530 would yield the same thing. Rather than apply a
// costly modulo operator, 1530 is handled as a special case below.
// So you'd think that the color "hexcone" (the thing that ramps from
// pure red, to pure yellow, to pure green and so forth back to red,
// yielding six slices), and with each color component having 256
// possible values (0-255), might have 1536 possible items (6*256),
// but in reality there's 1530. This is because the last element in
// each 256-element slice is equal to the first element of the next
// slice, and keeping those in there this would create small
// discontinuities in the color wheel. So the last element of each
// slice is dropped...we regard only elements 0-254, with item 255
// being picked up as element 0 of the next slice. Like this:
// Red to not-quite-pure-yellow is: 255, 0, 0 to 255, 254, 0
// Pure yellow to not-quite-pure-green is: 255, 255, 0 to 1, 255, 0
// Pure green to not-quite-pure-cyan is: 0, 255, 0 to 0, 255, 254
// and so forth. Hence, 1530 distinct hues (0 to 1529), and hence why
// the constants below are not the multiples of 256 you might expect.
// Convert hue to R,G,B (nested ifs faster than divide+mod+switch):
if (hue < 510) { // Red to Green-1
b = 0;
if (hue < 255) { // Red to Yellow-1
r = 255;
g = hue; // g = 0 to 254
} else { // Yellow to Green-1
r = 510 - hue; // r = 255 to 1
g = 255;
}
} else if (hue < 1020) { // Green to Blue-1
r = 0;
if (hue < 765) { // Green to Cyan-1
g = 255;
b = hue - 510; // b = 0 to 254
} else { // Cyan to Blue-1
g = 1020 - hue; // g = 255 to 1
b = 255;
}
} else if (hue < 1530) { // Blue to Red-1
g = 0;
if (hue < 1275) { // Blue to Magenta-1
r = hue - 1020; // r = 0 to 254
b = 255;
} else { // Magenta to Red-1
r = 255;
b = 1530 - hue; // b = 255 to 1
}
} else { // Last 0.5 Red (quicker than % operator)
r = 255;
g = b = 0;
}
// Apply saturation and value to R,G,B, pack into 32-bit result:
uint32_t v1 = 1 + val; // 1 to 256; allows >>8 instead of /255
uint16_t s1 = 1 + sat; // 1 to 256; same reason
uint8_t s2 = 255 - sat; // 255 to 0
return ((((((r * s1) >> 8) + s2) * v1) & 0xff00) << 8) |
(((((g * s1) >> 8) + s2) * v1) & 0xff00) |
(((((b * s1) >> 8) + s2) * v1) >> 8);
}

View File

@@ -0,0 +1,47 @@
#pragma once
const char JSON_MAP[] = R"({"map": [
406,407,408,409,410,411,412,413,414,415,416,417,418,419,
420,421,422,423,424,425,426,427,428,429,430,431,432,433,
434,435,436,437,438,439,440,441,442,443,444,445,446,447,
532,533,534,535,536,537,538,539,540,541,542,543,544,545,
546,547,548,549,550,551,552,553,554,555,556,557,558,559,
377,376,375,374,373,372,371,370,369,368,367,366,365,364,
363,362,361,360,359,358,357,356,355,354,353,352,351,350,
392,393,394,395,396,397,398,399,400,401,402,403,404,405,
223,222,221,220,219,218,217,216,215,214,213,212,211,210,
125,124,123,122,121,120,119,118,117,116,115,114,113,112,
111,110,109,108,107,106,105,104,103,102,101,100,99,98,
97,96,95,94,93,92,91,90,89,88,87,86,85,84,
168,169,170,171,172,173,174,175,176,177,178,179,180,181,
182,183,184,185,186,187,188,189,190,191,192,193,194,195,
196,197,198,199,200,201,202,203,204,205,206,207,208,209,
126,127,128,129,130,131,132,133,134,135,136,137,138,139,
307,306,305,304,303,302,301,300,299,298,297,296,295,294,
349,348,347,346,345,344,343,342,341,340,339,338,337,336,
391,390,389,388,387,386,385,384,383,382,381,380,379,378,
13,12,11,10,9,8,7,6,5,4,3,2,1,0,
461,460,459,458,457,456,455,454,453,452,451,450,449,448,
531,530,529,528,527,526,525,524,523,522,521,520,519,518,
517,516,515,514,513,512,511,510,509,508,507,506,505,504,
503,502,501,500,499,498,497,496,495,494,493,492,491,490,
476,477,478,479,480,481,482,483,484,485,486,487,488,489,
321,320,319,318,317,316,315,314,313,312,311,310,309,308,
153,152,151,150,149,148,147,146,145,144,143,142,141,140,
322,323,324,325,326,327,328,329,330,331,332,333,334,335,
475,474,473,472,471,470,469,468,467,466,465,464,463,462,
154,155,156,157,158,159,160,161,162,163,164,165,166,167,
14,15,16,17,18,19,20,21,22,23,24,25,26,27,
28,29,30,31,32,33,34,35,36,37,38,39,40,41,
42,43,44,45,46,47,48,49,50,51,52,53,54,55,
56,57,58,59,60,61,62,63,64,65,66,67,68,69,
70,71,72,73,74,75,76,77,78,79,80,81,82,83,
237,236,235,234,233,232,231,230,229,228,227,226,225,224,
238,239,240,241,242,243,244,245,246,247,248,249,250,251,
252,253,254,255,256,257,258,259,260,261,262,263,264,265,
266,267,268,269,270,271,272,273,274,275,276,277,278,279,
280,281,282,283,284,285,286,287,288,289,290,291,292,293
]}
)";

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,270 @@
"""
Generates the hexegon using math.
"""
from dataclasses import dataclass
from enum import Enum
import json
from math import pi, cos, sin
LED_PER_STRIP = 14
SPACE_PER_LED = 30.0 # Increased for better visibility
LED_DIAMETER = SPACE_PER_LED / 4
MIRROR_X = True # Diagramed from the reverse side. Reverse the x-axis
SMALLEST_ANGLE = 360 / 6
class HexagonAngle(Enum):
UP = 90
DOWN = 270
RIGHT_UP = 30
RIGHT_DOWN = 360 - 30
LEFT_UP = 150 # (RIGHT_DOWN + 180) % 360
LEFT_DOWN = 210 # (RIGHT_UP + 180) % 360
def toRads(angle: float) -> float:
return angle * (pi / 180)
@dataclass
class Point:
x: float
y: float
@staticmethod
def toJson(points: list["Point"]) -> list[dict]:
x_values = [p.x for p in points]
y_values = [p.y for p in points]
# round
x_values = [round(x, 4) for x in x_values]
y_values = [round(y, 4) for y in y_values]
if MIRROR_X:
x_values = [-x for x in x_values]
return {"x": x_values, "y": y_values, "diameter": LED_DIAMETER}
def copy(self) -> "Point":
return Point(self.x, self.y)
def __repr__(self) -> str:
x_rounded = round(self.x, 2)
y_rounded = round(self.y, 2)
return f"({x_rounded}, {y_rounded})"
def next_point(pos: Point, angle: HexagonAngle, space: float) -> Point:
degrees = angle.value
angle_rad = toRads(degrees)
x = pos.x + space * cos(angle_rad)
y = pos.y + space * sin(angle_rad)
return Point(x, y)
def gen_points(
input: list[HexagonAngle], leds_per_strip: int, startPos: Point,
exclude: list[int] | None = None,
add_last: bool = False
) -> list[Point]:
points: list[Point] = []
if (not input) or (not leds_per_strip):
return points
exclude = exclude or []
# Start FSM. Start pointer get's put into the accumulator.
curr_point: Point = Point(startPos.x, startPos.y)
# points.append(curr_point)
last_angle = input[0]
for i,angle in enumerate(input):
excluded = i in exclude
values = list(range(leds_per_strip))
last_angle = angle
for v in values:
last_angle = angle
curr_point = next_point(curr_point, angle, SPACE_PER_LED)
if not excluded:
points.append(curr_point)
#if i == len(input) - 1:
# break
# Next starting point
curr_point = next_point(curr_point, last_angle, SPACE_PER_LED)
#if not excluded:
# points.append(curr_point)
if add_last:
points.append(curr_point)
return points
def main() -> None:
startPos = Point(0, 0)
hexagon_angles = [
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.RIGHT_DOWN,
HexagonAngle.DOWN,
HexagonAngle.LEFT_DOWN,
HexagonAngle.LEFT_UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, startPos)
print(points)
def simple_test() -> None:
startPos = Point(0, 0)
hexagon_angles = [
HexagonAngle.UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, startPos)
print(points)
# assert len(points) == LED_PER_STRIP + 1
def two_angle_test() -> None:
startPos = Point(0, 0)
hexagon_angles = [
HexagonAngle.UP,
HexagonAngle.UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, startPos)
print(points)
# assert len(points) == LED_PER_STRIP * 2, f"Expected {LED_PER_STRIP * 2} points, got {len(points)} points"
def two_angle_test2() -> None:
print("two_angle_test2")
startPos = Point(0, 0)
hexagon_angles = [
HexagonAngle.UP,
HexagonAngle.DOWN,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, startPos)
print(points)
# assert len(points) == LED_PER_STRIP * 2, f"Expected {LED_PER_STRIP * 2} points, got {len(points)} points"
# Red is defined by this instruction tutorial: https://voidstar.dozuki.com/Guide/Chromance+Assembly+Instructions/6
def find_red_anchor_point() -> list[Point]:
hexagon_angles = [
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_UP,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, Point(0, 0), add_last=True)
return points
def find_green_anchore_point() -> list[Point]:
hexagon_angles = [
HexagonAngle.RIGHT_UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, Point(0, 0), add_last=True)
return points
RED_ANCHOR_POINT = find_red_anchor_point()[-1]
BLACK_ANCHOR_POINT = Point(0,0) # Black
GREEN_ANCHOR_POINT = find_green_anchore_point()[-1]
BLUE_ANCHOR_POINT = Point(0, 0)
def generate_red_points() -> list[Point]:
starting_point = RED_ANCHOR_POINT.copy()
hexagon_angles = [
HexagonAngle.UP,
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_DOWN,
HexagonAngle.DOWN,
HexagonAngle.RIGHT_DOWN,
HexagonAngle.UP,
HexagonAngle.LEFT_UP
]
points = gen_points(hexagon_angles, LED_PER_STRIP, starting_point, exclude=[5])
return points
def generate_black_points() -> list[Point]:
starting_point = BLACK_ANCHOR_POINT.copy()
hexagon_angles = [
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_UP,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.RIGHT_DOWN,
HexagonAngle.DOWN,
HexagonAngle.LEFT_DOWN,
HexagonAngle.UP,
HexagonAngle.LEFT_UP,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, starting_point)
return points
def generate_green_points() -> list[Point]:
starting_point = GREEN_ANCHOR_POINT.copy()
hexagon_angles = [
HexagonAngle.RIGHT_UP,
HexagonAngle.UP,
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_DOWN,
HexagonAngle.DOWN,
HexagonAngle.RIGHT_DOWN, # skip
HexagonAngle.LEFT_DOWN, # skip
HexagonAngle.LEFT_UP,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_DOWN,
HexagonAngle.RIGHT_DOWN,
HexagonAngle.RIGHT_UP, # skip
HexagonAngle.RIGHT_DOWN,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, starting_point, exclude=[5,6,13])
return points
def generate_blue_points() -> list[Point]:
starting_point = BLUE_ANCHOR_POINT.copy()
hexagon_angles = [
HexagonAngle.RIGHT_UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.UP,
HexagonAngle.LEFT_UP,
HexagonAngle.LEFT_DOWN,
HexagonAngle.LEFT_DOWN,
HexagonAngle.RIGHT_DOWN, # skip
HexagonAngle.RIGHT_DOWN,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
HexagonAngle.UP,
HexagonAngle.RIGHT_UP,
]
points = gen_points(hexagon_angles, LED_PER_STRIP, starting_point, exclude=[6])
return points
def unit_test() -> None:
#simple_test()
#two_angle_test()
out = {}
map = out.setdefault("map", {})
map.update({
"red_segment": Point.toJson(generate_red_points()),
"back_segment": Point.toJson(generate_black_points()),
"green_segment": Point.toJson(generate_green_points()),
"blue_segment": Point.toJson(generate_blue_points()),
})
print(json.dumps(out))
# write it out to a file
with open("output.json", "w") as f:
f.write(json.dumps(out))
if __name__ == "__main__":
unit_test()

View File

@@ -0,0 +1,158 @@
/*
* Maps hex topology onto LED's
* (C) Voidstar Lab LLC 2021
*/
#ifndef MAPPING_H_
#define MAPPING_H_
// I accidentally noted these down 1-indexed and I'm too tired to adjust them
#define headof(S) ((S - 1) * 14)
#define tailof(S) (headof(S) + 13)
// Beam 0 is at 12:00 and advance clockwise
// -1 means nothing connected on that side
int nodeConnections[25][6] = {
{-1, -1, 1, -1, 0, -1},
{-1, -1, 3, -1, 2, -1},
{-1, -1, 5, -1, 4, -1},
{-1, 0, 6, 12, -1, -1},
{-1, 2, 8, 14, 7, 1},
{-1, 4, 10, 16, 9, 3},
{-1, -1, -1, 18, 11, 5},
{-1, 7, -1, 13, -1, 6},
{-1, 9, -1, 15, -1, 8},
{-1, 11, -1, 17, -1, 10},
{12, -1, 19, -1, -1, -1},
{14, -1, 21, -1, 20, -1},
{16, -1, 23, -1, 22, -1},
{18, -1, -1, -1, 24, -1},
{13, 20, 25, 29, -1, -1},
{15, 22, 27, 31, 26, 21},
{17, 24, -1, 33, 28, 23},
{-1, 26, -1, 30, -1, 25},
{-1, 28, -1, 32, -1, 27},
{29, -1, 34, -1, -1, -1},
{31, -1, 36, -1, 35, -1},
{33, -1, -1, -1, 37, -1},
{30, 35, 38, -1, -1, 34},
{32, 37, -1, -1, 39, 36},
{-1, 39, -1, -1, -1, 38}
};
// First member: Node closer to ceiling
// Second: Node closer to floor
int segmentConnections[40][2] = {
{0, 3},
{0, 4},
{1, 4},
{1, 5},
{2, 5},
{2, 6},
{3, 7},
{4, 7},
{4, 8},
{5, 8},
{5, 9},
{6, 9}, // ayy
{3, 10},
{7, 14},
{4, 11},
{8, 15},
{5, 12},
{9, 16},
{6, 13},
{10, 14},
{11, 14},
{11, 15},
{12, 15},
{12, 16},
{13, 16},
{14, 17},
{15, 17},
{15, 18},
{16, 18},
{14, 19},
{17, 22},
{15, 20},
{18, 23},
{16, 21},
{19, 22},
{20, 22},
{20, 23},
{21, 23},
{22, 24},
{23, 24}
};
// First member: Strip number
// Second: LED index closer to ceiling
// Third: LED index closer to floor
int ledAssignments[40][3] = {
{2, headof(3), tailof(3)},
{2, tailof(2), headof(2)},
{1, headof(10), tailof(10)},
{1, tailof(9), headof(9)},
{1, headof(4), tailof(4)},
{1, tailof(3), headof(3)},
{2, tailof(6), headof(6)},
{3, tailof(11), headof(11)},
{1, headof(11), tailof(11)},
{1, tailof(8), headof(8)},
{1, headof(12), tailof(12)},
{0, tailof(11), headof(11)},
{2, headof(4), tailof(4)},
{3, tailof(10), headof(10)},
{2, tailof(1), headof(1)},
{1, tailof(7), headof(7)},
{1, headof(5), tailof(5)},
{0, tailof(10), headof(10)},
{1, tailof(2), headof(2)},
{2, headof(5), tailof(5)},
{3, tailof(4), headof(4)},
{3, headof(5), tailof(5)},
{0, headof(5), tailof(5)},
{0, tailof(4), headof(4)},
{1, tailof(1), headof(1)},
{3, tailof(9), headof(9)},
{0, headof(6), tailof(6)},
{1, tailof(6), headof(6)},
{0, tailof(9), headof(9)},
{3, tailof(3), headof(3)},
{3, tailof(8), headof(8)},
{3, headof(6), tailof(6)},
{0, tailof(8), headof(8)},
{0, tailof(3), headof(3)},
{3, tailof(2), headof(2)},
{3, headof(7), tailof(7)},
{0, headof(7), tailof(7)},
{0, tailof(2), headof(2)},
{3, tailof(1), headof(1)},
{0, tailof(1), headof(1)}
};
// Border nodes are on the very edge of the network.
// Ripples fired here don't look very impressive.
int numberOfBorderNodes = 10;
int borderNodes[] = {0, 1, 2, 3, 6, 10, 13, 19, 21, 24};
// Cube nodes link three equiangular segments
// Firing ripples that always turn in one direction will draw a cube
int numberOfCubeNodes = 8;
int cubeNodes[] = {7, 8, 9, 11, 12, 17, 18};
// Firing ripples that always turn in one direction will draw a starburst
int starburstNode = 15;
#endif

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,426 @@
/*
A dot animation that travels along rails
(C) Voidstar Lab LLC 2021
*/
#ifndef RIPPLE_H_
#define RIPPLE_H_
// WARNING: These slow things down enough to affect performance. Don't turn on unless you need them!
//#define DEBUG_ADVANCEMENT // Print debug messages about ripples' movement
//#define DEBUG_RENDERING // Print debug messages about translating logical to actual position
#include "FastLED.h"
#include "mapping.h"
enum rippleState {
dead,
withinNode, // Ripple isn't drawn as it passes through a node to keep the speed consistent
travelingUpwards,
travelingDownwards
};
enum rippleBehavior {
weaksauce = 0,
feisty = 1,
angry = 2,
alwaysTurnsRight = 3,
alwaysTurnsLeft = 4
};
float fmap(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
class Ripple {
public:
Ripple(int id) : rippleId(id) {
Serial.print("Instanced ripple #");
Serial.println(rippleId);
}
rippleState state = dead;
unsigned long color;
/*
If within a node: 0 is node, 1 is direction
If traveling, 0 is segment, 1 is LED position from bottom
*/
int position[2];
// Place the Ripple in a node
void start(byte n, byte d, unsigned long c, float s, unsigned long l, byte b) {
color = c;
speed = s;
lifespan = l;
behavior = b;
birthday = millis();
pressure = 0;
state = withinNode;
position[0] = n;
position[1] = d;
justStarted = true;
Serial.print("Ripple ");
Serial.print(rippleId);
Serial.print(" starting at node ");
Serial.print(position[0]);
Serial.print(" direction ");
Serial.println(position[1]);
}
void advance(byte ledColors[40][14][3]) {
unsigned long age = millis() - birthday;
if (state == dead)
return;
pressure += fmap(float(age), 0.0, float(lifespan), speed, 0.0); // Ripple slows down as it ages
// TODO: Motion of ripple is severely affected by loop speed. Make it time invariant
if (pressure < 1 && (state == travelingUpwards || state == travelingDownwards)) {
// Ripple is visible but hasn't moved - render it to avoid flickering
renderLed(ledColors, age);
}
while (pressure >= 1) {
#ifdef DEBUG_ADVANCEMENT
Serial.print("Ripple ");
Serial.print(rippleId);
Serial.println(" advancing:");
#endif
switch (state) {
case withinNode: {
if (justStarted) {
justStarted = false;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Picking direction out of node ");
Serial.print(position[0]);
Serial.print(" with agr. ");
Serial.println(behavior);
#endif
int newDirection = -1;
int sharpLeft = (position[1] + 1) % 6;
int wideLeft = (position[1] + 2) % 6;
int forward = (position[1] + 3) % 6;
int wideRight = (position[1] + 4) % 6;
int sharpRight = (position[1] + 5) % 6;
if (behavior <= 2) { // Semi-random aggressive turn mode
// The more aggressive a ripple, the tighter turns it wants to make.
// If there aren't any segments it can turn to, we need to adjust its behavior.
byte anger = behavior;
while (newDirection < 0) {
if (anger == 0) {
int forwardConnection = nodeConnections[position[0]][forward];
if (forwardConnection < 0) {
// We can't go straight ahead - we need to take a more aggressive angle
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can't go straight - picking more agr. path");
#endif
anger++;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Going forward");
#endif
newDirection = forward;
}
}
if (anger == 1) {
int leftConnection = nodeConnections[position[0]][wideLeft];
int rightConnection = nodeConnections[position[0]][wideRight];
if (leftConnection >= 0 && rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Turning left or right at random");
#endif
newDirection = random(2) ? wideLeft : wideRight;
}
else if (leftConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can only turn left");
#endif
newDirection = wideLeft;
}
else if (rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can only turn right");
#endif
newDirection = wideRight;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can't make wide turn - picking more agr. path");
#endif
anger++; // Can't take shallow turn - must become more aggressive
}
}
if (anger == 2) {
int leftConnection = nodeConnections[position[0]][sharpLeft];
int rightConnection = nodeConnections[position[0]][sharpRight];
if (leftConnection >= 0 && rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Turning left or right at random");
#endif
newDirection = random(2) ? sharpLeft : sharpRight;
}
else if (leftConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can only turn left");
#endif
newDirection = sharpLeft;
}
else if (rightConnection >= 0) {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can only turn right");
#endif
newDirection = sharpRight;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Can't make tight turn - picking less agr. path");
#endif
anger--; // Can't take tight turn - must become less aggressive
}
}
// Note that this can't handle some circumstances,
// like a node with segments in nothing but the 0 and 3 positions.
// Good thing we don't have any of those!
}
}
else if (behavior == alwaysTurnsRight) {
for (int i = 1; i < 6; i++) {
int possibleDirection = (position[1] + i) % 6;
if (nodeConnections[position[0]][possibleDirection] >= 0) {
newDirection = possibleDirection;
break;
}
}
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Turning as rightward as possible");
#endif
}
else if (behavior == alwaysTurnsLeft) {
for (int i = 5; i >= 1; i--) {
int possibleDirection = (position[1] + i) % 6;
if (nodeConnections[position[0]][possibleDirection] >= 0) {
newDirection = possibleDirection;
break;
}
}
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Turning as leftward as possible");
#endif
}
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Leaving node ");
Serial.print(position[0]);
Serial.print(" in direction ");
Serial.println(newDirection);
#endif
position[1] = newDirection;
}
position[0] = nodeConnections[position[0]][position[1]]; // Look up which segment we're on
#ifdef DEBUG_ADVANCEMENT
Serial.print(" and entering segment ");
Serial.println(position[0]);
#endif
if (position[1] == 5 || position[1] == 0 || position[1] == 1) { // Top half of the node
#ifdef DEBUG_ADVANCEMENT
Serial.println(" (starting at bottom)");
#endif
state = travelingUpwards;
position[1] = 0; // Starting at bottom of segment
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.println(" (starting at top)");
#endif
state = travelingDownwards;
position[1] = 13; // Starting at top of 14-LED-long strip
}
break;
}
case travelingUpwards: {
position[1]++;
if (position[1] >= 14) {
// We've reached the top!
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Reached top of seg. ");
Serial.println(position[0]);
#endif
// Enter the new node.
int segment = position[0];
position[0] = segmentConnections[position[0]][0];
for (int i = 0; i < 6; i++) {
// Figure out from which direction the ripple is entering the node.
// Allows us to exit in an appropriately aggressive direction.
int incomingConnection = nodeConnections[position[0]][i];
if (incomingConnection == segment)
position[1] = i;
}
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Entering node ");
Serial.print(position[0]);
Serial.print(" from direction ");
Serial.println(position[1]);
#endif
state = withinNode;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Moved up to seg. ");
Serial.print(position[0]);
Serial.print(" LED ");
Serial.println(position[1]);
#endif
}
break;
}
case travelingDownwards: {
position[1]--;
if (position[1] < 0) {
// We've reached the bottom!
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Reached bottom of seg. ");
Serial.println(position[0]);
#endif
// Enter the new node.
int segment = position[0];
position[0] = segmentConnections[position[0]][1];
for (int i = 0; i < 6; i++) {
// Figure out from which direction the ripple is entering the node.
// Allows us to exit in an appropriately aggressive direction.
int incomingConnection = nodeConnections[position[0]][i];
if (incomingConnection == segment)
position[1] = i;
}
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Entering node ");
Serial.print(position[0]);
Serial.print(" from direction ");
Serial.println(position[1]);
#endif
state = withinNode;
}
else {
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Moved down to seg. ");
Serial.print(position[0]);
Serial.print(" LED ");
Serial.println(position[1]);
#endif
}
break;
}
default:
break;
}
pressure -= 1;
if (state == travelingUpwards || state == travelingDownwards) {
// Ripple is visible - render it
renderLed(ledColors, age);
}
}
#ifdef DEBUG_ADVANCEMENT
Serial.print(" Age is now ");
Serial.print(age);
Serial.print('/');
Serial.println(lifespan);
#endif
if (lifespan && age >= lifespan) {
// We dead
#ifdef DEBUG_ADVANCEMENT
Serial.println(" Lifespan is up! Ripple is dead.");
#endif
state = dead;
position[0] = position[1] = pressure = age = 0;
}
}
private:
float speed; // Each loop, ripples move this many LED's.
unsigned long lifespan; // The ripple stops after this many milliseconds
/*
0: Always goes straight ahead if possible
1: Can take 60-degree turns
2: Can take 120-degree turns
*/
byte behavior;
bool justStarted = false;
float pressure; // When Pressure reaches 1, ripple will move
unsigned long birthday; // Used to track age of ripple
static byte rippleCount; // Used to give them unique ID's
byte rippleId; // Used to identify this ripple in debug output
void renderLed(byte ledColors[40][14][3], unsigned long age) {
int strip = ledAssignments[position[0]][0];
int led = ledAssignments[position[0]][2] + position[1];
FL_UNUSED(strip);
FL_UNUSED(led);
int red = ledColors[position[0]][position[1]][0];
int green = ledColors[position[0]][position[1]][1];
int blue = ledColors[position[0]][position[1]][2];
ledColors[position[0]][position[1]][0] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 8) & 0xFF, 0.0)) + red)));
ledColors[position[0]][position[1]][1] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), (color >> 16) & 0xFF, 0.0)) + green)));
ledColors[position[0]][position[1]][2] = byte(min(255, max(0, int(fmap(float(age), 0.0, float(lifespan), color & 0xFF, 0.0)) + blue)));
#ifdef DEBUG_RENDERING
Serial.print("Rendering ripple position (");
Serial.print(position[0]);
Serial.print(',');
Serial.print(position[1]);
Serial.print(") at Strip ");
Serial.print(strip);
Serial.print(", LED ");
Serial.print(led);
Serial.print(", color 0x");
for (int i = 0; i < 3; i++) {
if (ledColors[position[0]][position[1]][i] <= 0x0F)
Serial.print('0');
Serial.print(ledColors[position[0]][position[1]][i], HEX);
}
Serial.println();
#endif
}
};
#endif

File diff suppressed because one or more lines are too long