#include <FastLED.h>
#define LED_PIN 5
#define NUM_LEDS 230
#define CHIPSET WS2812B
#define COLOR_ORDER GRB
#define MODE_PIN 2
#define BRIGHT_PIN 3
CRGB leds[NUM_LEDS];
// ==================================================
// PROTOTYPES
// ==================================================
void redBlueWave();
void centerOutWaveColors();
void embers();
void voidPulse();
void skirtSparkleWhite();
void deepColorFade();
void poisonBubbles();
void deepTwinkle();
void waterRipples();
// ==================================================
// STATE
// ==================================================
uint8_t currentBrightness = 90;
const uint8_t BRIGHTNESS_MIN = 5;
const uint8_t BRIGHTNESS_MAX = 160;
byte mode = 0;
const byte NUM_MODES = 9;
bool brightnessMode = false;
bool lastModeState = HIGH;
bool lastBrightState = HIGH;
unsigned long lastTapTime = 0;
const unsigned long doubleTapWindow = 350;
const uint8_t BRIGHTNESS_STEP = 5;
const uint16_t BRIGHTNESS_REPEAT_RATE = 60;
unsigned long lastBrightStepTime = 0;
// ==================================================
// BPM
// ==================================================
float bpm = 0.0;
float BPM_MULTIPLIER = 1.0;
float BPM_PULSE_DEPTH = 0.25;
uint8_t BPM_FLASH_BRIGHT = 200;
bool bpmSetMode = false;
unsigned long brightHoldStart = 0;
#define TAP_BUFFER 4
unsigned long tapTimes[TAP_BUFFER];
uint8_t tapIndex = 0;
unsigned long lastBeatTime = 0;
// ==================================================
// EFFECT CONTROLS
// ==================================================
// --- Red Blue Wave ---
float RBW_SPEED = 0.10;
float RBW_SPATIAL = 0.12;
// --- Center Out Wave ---
float COW_REVEAL_SPEED = 0.6;
uint8_t COW_FADE = 1;
// --- Embers ---
uint8_t EMBER_DENSITY = 4;
uint8_t EMBER_FADE = 3;
float EMBER_SPATIAL_1 = 0.05;
float EMBER_SPATIAL_2 = 0.11;
float EMBER_SPEED = 0.3;
uint8_t EMBER_HOT = 130;
uint8_t EMBER_MID = 100;
// --- Void Pulse ---
uint8_t VP_FADE = 10;
float VP_RADIUS_RATIO = 0.15;
float VP_SPEED = 1.0;
// --- Skirt Sparkle ---
float SKIRT_SPEED = 0.75;
float SKIRT_WIDTH_RATIO = 0.55;
uint8_t SKIRT_SPARKLE_CHANCE = 3;
uint8_t SKIRT_SPARKLE_MIN = 80;
uint8_t SKIRT_SPARKLE_MAX = 140;
// --- Deep Color Fade ---
float DCF_SPEED = 0.0006;
// --- Poison Bubbles ---
const CRGB POISON_BASE = CRGB(55,0,85);
uint8_t POISON_SPAWN_CHANCE = 35;
uint8_t POISON_MAX_BUBBLES = 6;
uint8_t POISON_SIZE_MIN = 4;
uint8_t POISON_SIZE_MAX = 8;
uint8_t POISON_BLUR = 42;
// --- Deep Twinkle ---
uint8_t DT_FADE = 10;
uint8_t DT_BLUE_MAX = 180;
uint8_t DT_PURPLE_MAX = 140;
uint8_t DT_SPAWN_CHANCE = 100;
const uint8_t DT_MAX_STARS = 30;
// --- Water Ripples ---
const uint8_t WR_MAX_RIPPLES = 5;
float WR_SPEED_MIN = 0.55;
float WR_SPEED_MAX = 0.95;
float WR_WIDTH = 28.0;
uint8_t WR_FADE = 18;
uint8_t WR_SPAWN_CHANCE = 35;
// color ranges
uint8_t WR_BLUE_MIN = 140;
uint8_t WR_BLUE_MAX = 255;
uint8_t WR_PURPLE_MIN = 20;
uint8_t WR_PURPLE_MAX = 120;
// ripple brightness
uint8_t WR_BRIGHTNESS_MIN = 80;
uint8_t WR_BRIGHTNESS_MAX = 180;
// background
CRGB WR_BACKGROUND = CRGB(2,0,8);
// blur amount
uint8_t WR_BLUR = 28;
// ==================================================
float phase = 0.0;
unsigned long lastUpdate = 0;
// ==================================================
void setup() {
pinMode(MODE_PIN, INPUT_PULLUP);
pinMode(BRIGHT_PIN, INPUT_PULLUP);
FastLED.addLeds<CHIPSET, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(currentBrightness);
lastUpdate = millis();
}
// ==================================================
void loop() {
unsigned long now = millis();
float delta = now - lastUpdate;
lastUpdate = now;
phase += delta / 500.0;
handleButtons();
if (brightnessMode) {
updateBrightness();
showBrightnessPreview();
} else {
runEffect();
}
applyBPMPulse();
FastLED.show();
}
// ==================================================
void handleButtons() {
bool modeState = digitalRead(MODE_PIN);
bool brightState = digitalRead(BRIGHT_PIN);
unsigned long now = millis();
if (brightState == LOW) {
if (brightHoldStart == 0)
brightHoldStart = now;
if (!bpmSetMode && (now - brightHoldStart > 500)) {
bpmSetMode = true;
tapIndex = 0;
}
} else {
if (bpmSetMode)
finalizeBPM();
bpmSetMode = false;
brightHoldStart = 0;
}
if (lastModeState == HIGH && modeState == LOW) {
if (bpmSetMode) {
registerTap();
} else if (!brightnessMode) {
mode = (mode + 1) % NUM_MODES;
fill_solid(leds, NUM_LEDS, CRGB::Black);
}
}
if (lastBrightState == HIGH && brightState == LOW) {
if (!bpmSetMode &&
(now - lastTapTime < doubleTapWindow)) {
brightnessMode = !brightnessMode;
}
lastTapTime = now;
}
lastModeState = modeState;
lastBrightState = brightState;
}
// ==================================================
void updateBrightness() {
bool modeHeld = digitalRead(MODE_PIN) == LOW;
bool brightHeld = digitalRead(BRIGHT_PIN) == LOW;
unsigned long now = millis();
if (modeHeld || brightHeld) {
if (now - lastBrightStepTime > BRIGHTNESS_REPEAT_RATE) {
lastBrightStepTime = now;
if (modeHeld)
currentBrightness = min(BRIGHTNESS_MAX,
currentBrightness + BRIGHTNESS_STEP);
if (brightHeld)
currentBrightness = max(BRIGHTNESS_MIN,
currentBrightness - BRIGHTNESS_STEP);
FastLED.setBrightness(currentBrightness);
}
} else {
lastBrightStepTime = now;
}
}
// ==================================================
void showBrightnessPreview() {
fill_solid(leds, NUM_LEDS, CRGB(160,180,220));
}
// ==================================================
void registerTap() {
tapTimes[tapIndex % TAP_BUFFER] = millis();
tapIndex++;
fill_solid(leds, NUM_LEDS, CRGB(180,200,255));
FastLED.show();
delay(30);
}
// ==================================================
void finalizeBPM() {
if (tapIndex < 2) {
bpm = 0;
return;
}
float total = 0;
int valid = 0;
for (int i = 1; i < tapIndex; i++) {
unsigned long dt = tapTimes[i] - tapTimes[i-1];
if (dt > 150 && dt < 2000) {
total += dt;
valid++;
}
}
if (valid == 0) {
bpm = 0;
return;
}
bpm = 60000.0 / (total / valid);
lastBeatTime = millis();
}
// ==================================================
void applyBPMPulse() {
if (bpm <= 0) {
FastLED.setBrightness(currentBrightness);
return;
}
float interval = 60000.0 / (bpm * BPM_MULTIPLIER);
float t = (millis() - lastBeatTime) / interval;
float wave = sin(t * TWO_PI);
float scale = 1.0 + (wave * BPM_PULSE_DEPTH);
uint8_t finalB =
constrain(currentBrightness * scale,
BRIGHTNESS_MIN,
BRIGHTNESS_MAX);
FastLED.setBrightness(finalB);
}
// ==================================================
void runEffect() {
switch (mode) {
case 0: redBlueWave(); break;
case 1: centerOutWaveColors(); break;
case 2: embers(); break;
case 3: voidPulse(); break;
case 4: skirtSparkleWhite(); break;
case 5: deepColorFade(); break;
case 6: poisonBubbles(); break;
case 7: deepTwinkle(); break;
case 8: waterRipples(); break;
}
}
// ==================================================
// EFFECTS
// ==================================================
void redBlueWave() {
float t = phase * RBW_SPEED;
for (int i = 0; i < NUM_LEDS; i++) {
float wave =
sin((i * RBW_SPATIAL) + (t * TWO_PI));
float blend = (wave + 1.0) * 0.5;
leds[i] = CRGB(
blend * 255,
0,
(1.0 - blend) * 255
);
}
}
// ==================================================
void centerOutWaveColors() {
int center = NUM_LEDS / 2;
static float reveal = 0;
static byte state = 0;
reveal += COW_REVEAL_SPEED;
if (reveal >= center) {
reveal = 0;
state = (state + 1) % 4;
}
fadeToBlackBy(leds, NUM_LEDS, COW_FADE);
CRGB colors[4] = {
CRGB(255,0,255),
CRGB(0,180,255),
CRGB(255,150,0),
CRGB(0,255,90)
};
for (int i = 0; i < NUM_LEDS; i++) {
if (abs(i - center) <= reveal)
leds[i] += colors[state];
}
}
// ==================================================
void embers() {
fadeToBlackBy(leds, NUM_LEDS, EMBER_FADE);
float t = phase * EMBER_SPEED;
for (int i = 0; i < NUM_LEDS; i++) {
if (random8() < EMBER_DENSITY) {
leds[i] += CRGB(255, 90, 0);
}
}
}
// ==================================================
void voidPulse() {
fadeToBlackBy(leds, NUM_LEDS, VP_FADE);
float pos =
(sin(phase * VP_SPEED) + 1.0)
* 0.5 * NUM_LEDS;
int radius = NUM_LEDS * VP_RADIUS_RATIO;
for (int i = 0; i < NUM_LEDS; i++) {
float d = abs(i - pos);
if (d < radius) {
float v = 1.0 - (d / radius);
leds[i] += CHSV(200,255,v*255);
}
}
}
// ==================================================
void skirtSparkleWhite() {
fill_solid(leds, NUM_LEDS, CRGB(2,2,6));
float t = phase * SKIRT_SPEED;
float center = fmod(t * 40.0, NUM_LEDS);
int width = NUM_LEDS * SKIRT_WIDTH_RATIO;
for (int i = 0; i < NUM_LEDS; i++) {
float dist = abs(i - center);
if (dist > NUM_LEDS / 2)
dist = NUM_LEDS - dist;
if (dist < width) {
float falloff = 1.0 - (dist / width);
falloff *= falloff;
leds[i] += CRGB(
120 * falloff,
160 * falloff,
255 * falloff
);
}
if (random8() < SKIRT_SPARKLE_CHANCE) {
uint8_t sparkle =
random8(SKIRT_SPARKLE_MIN,
SKIRT_SPARKLE_MAX);
leds[i] += CRGB(
sparkle / 3,
sparkle / 2,
sparkle
);
}
}
blur1d(leds, NUM_LEDS, 18);
}
// ==================================================
void deepColorFade() {
fill_solid(leds, NUM_LEDS, CRGB(200,0,100));
}
// ==================================================
void poisonBubbles() {
const int center = NUM_LEDS / 2;
static float pos[6];
static float speed[6];
static float size[6];
static bool active[6];
fill_solid(leds, NUM_LEDS, POISON_BASE);
if (random8() < POISON_SPAWN_CHANCE) {
for (int i = 0; i < POISON_MAX_BUBBLES; i++) {
if (!active[i]) {
active[i] = true;
pos[i] = 0;
speed[i] =
random(80,120)/100.0;
size[i] =
random(POISON_SIZE_MIN,
POISON_SIZE_MAX);
break;
}
}
}
for (int i = 0; i < POISON_MAX_BUBBLES; i++) {
if (!active[i]) continue;
pos[i] += speed[i];
int offset = (int)pos[i];
if (offset > center) {
active[i] = false;
continue;
}
for (int dir = -1; dir <= 1; dir += 2) {
int p = center + dir * offset;
for (int j = -size[i];
j <= size[i];
j++) {
int idx = p + j;
if (idx < 0 || idx >= NUM_LEDS)
continue;
float d = abs(j);
float f = 1.0 - (d / size[i]);
f *= f;
leds[idx] += CRGB(
10 * f,
120 + 135 * f,
30 * f
);
}
}
}
blur1d(leds, NUM_LEDS, POISON_BLUR);
}
// ==================================================
void deepTwinkle() {
fadeToBlackBy(leds, NUM_LEDS, DT_FADE);
static int pos[DT_MAX_STARS];
static float life[DT_MAX_STARS];
static float speed[DT_MAX_STARS];
static bool active[DT_MAX_STARS];
if (random8() < DT_SPAWN_CHANCE) {
for (int i = 0; i < DT_MAX_STARS; i++) {
if (!active[i]) {
active[i] = true;
pos[i] = random16(NUM_LEDS);
life[i] = 0.0;
speed[i] =
random(5, 15) / 100.0;
break;
}
}
}
for (int i = 0; i < DT_MAX_STARS; i++) {
if (!active[i]) continue;
life[i] += speed[i];
if (life[i] >= 1.0) {
active[i] = false;
continue;
}
float b = sin(life[i] * PI);
uint8_t blue = b * DT_BLUE_MAX;
uint8_t red = b * (DT_PURPLE_MAX / 2);
leds[pos[i]] += CRGB(red, 0, blue);
}
blur1d(leds, NUM_LEDS, 20);
}
// ==================================================
void waterRipples() {
fill_solid(leds, NUM_LEDS, WR_BACKGROUND);
static float radius[WR_MAX_RIPPLES];
static float speed[WR_MAX_RIPPLES];
static int center[WR_MAX_RIPPLES];
static uint8_t blue[WR_MAX_RIPPLES];
static uint8_t purple[WR_MAX_RIPPLES];
static uint8_t bright[WR_MAX_RIPPLES];
static bool active[WR_MAX_RIPPLES];
if (random8() < WR_SPAWN_CHANCE) {
for (int i = 0; i < WR_MAX_RIPPLES; i++) {
if (!active[i]) {
active[i] = true;
radius[i] = 0;
center[i] = random16(NUM_LEDS);
speed[i] =
random(WR_SPEED_MIN * 100,
WR_SPEED_MAX * 100) / 100.0;
blue[i] =
random8(WR_BLUE_MIN,
WR_BLUE_MAX);
purple[i] =
random8(WR_PURPLE_MIN,
WR_PURPLE_MAX);
bright[i] =
random8(WR_BRIGHTNESS_MIN,
WR_BRIGHTNESS_MAX);
break;
}
}
}
for (int i = 0; i < WR_MAX_RIPPLES; i++) {
if (!active[i]) continue;
radius[i] += speed[i];
if (radius[i] > NUM_LEDS / 2) {
active[i] = false;
continue;
}
for (int j = 0; j < NUM_LEDS; j++) {
float d = abs(j - center[i]);
if (d > NUM_LEDS / 2)
d = NUM_LEDS - d;
float edge = abs(d - radius[i]);
if (edge < WR_WIDTH) {
float wave = 1.0 - (edge / WR_WIDTH);
wave *= wave;
uint8_t b = wave * bright[i];
leds[j] += CRGB(
(purple[i] * b) / 255,
0,
(blue[i] * b) / 255
);
}
}
}
blur1d(leds, NUM_LEDS, WR_BLUR);
}