#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];
// ==================================================
// STATE
// ==================================================
uint8_t currentBrightness = 90;
const uint8_t BRIGHTNESS_MIN = 5;
const uint8_t BRIGHTNESS_MAX = 160;
byte mode = 0;
const byte NUM_MODES = 7;
bool brightnessMode = false;
bool lastModeState = HIGH;
bool lastBrightState = HIGH;
unsigned long lastTapTime = 0;
const unsigned long doubleTapWindow = 350;
// brightness stepping
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 = 0.94;
float BPM_PULSE_DEPTH = 0.25;
uint8_t BPM_FLASH_BRIGHT = 120;
bool bpmSetMode = false;
unsigned long brightHoldStart = 0;
#define TAP_BUFFER 4
unsigned long tapTimes[TAP_BUFFER];
uint8_t tapIndex = 0;
unsigned long lastBeatTime = 0;
// ==================================================
float phase = 0.0;
unsigned long lastUpdate = 0;
// ==================================================
void setup() {
pinMode(MODE_PIN, INPUT_PULLUP);
pinMode(BRIGHT_PIN, INPUT_PULLUP);
Serial.begin(115200);
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();
// --- HOLD FOR BPM ---
if (brightState == LOW) {
if (brightHoldStart == 0) brightHoldStart = now;
if (!bpmSetMode && (now - brightHoldStart > 500)) {
bpmSetMode = true;
tapIndex = 0;
Serial.println("BPM TAP MODE");
}
} else {
if (bpmSetMode) finalizeBPM();
bpmSetMode = false;
brightHoldStart = 0;
}
// --- MODE BUTTON ---
if (lastModeState == HIGH && modeState == LOW) {
if (bpmSetMode) {
registerTap();
}
else if (!brightnessMode) {
mode = (mode + 1) % NUM_MODES;
fill_solid(leds, NUM_LEDS, CRGB::Black);
}
}
// --- BRIGHT BUTTON (double tap) ---
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);
Serial.print("Brightness: ");
Serial.println(currentBrightness);
}
} else {
lastBrightStepTime = now;
}
}
// ==================================================
void showBrightnessPreview() {
fill_solid(leds, NUM_LEDS, CRGB(160,180,220));
}
// ==================================================
// BPM TAP
// ==================================================
void registerTap() {
unsigned long now = millis();
tapTimes[tapIndex % TAP_BUFFER] = now;
tapIndex++;
FastLED.setBrightness(BPM_FLASH_BRIGHT);
fill_solid(leds, NUM_LEDS, CRGB(180,200,255));
FastLED.show();
delay(30);
FastLED.setBrightness(currentBrightness);
Serial.println("Tap");
}
// ==================================================
void finalizeBPM() {
if (tapIndex < 2) {
bpm = 0;
Serial.println("BPM cleared");
return;
}
int count = min(tapIndex, TAP_BUFFER);
float total = 0;
int valid = 0;
for (int i = 1; i < count; 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);
Serial.print("BPM: ");
Serial.println(bpm);
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;
}
}
// ==================================================
// FIXED SKIRT SPARKLE
// ==================================================
void skirtSparkleWhite() {
fill_solid(leds, NUM_LEDS, CRGB(2,2,6));
float t = phase * 0.75;
float center = fmod(t * 40.0, NUM_LEDS);
int width = NUM_LEDS * 0.55;
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() < 3) {
uint8_t sparkle = random8(80,140);
leds[i] += CRGB(
sparkle / 3,
sparkle / 2,
sparkle
);
}
}
blur1d(leds, NUM_LEDS, 18);
}
// ==================================================
// FIXED POISON BUBBLES
// ==================================================
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, CRGB(55,0,85));
if (random8() < 35) {
for (int i = 0; i < 6; i++) {
if (!active[i]) {
active[i] = true;
pos[i] = 0;
speed[i] = random(80,120)/100.0;
size[i] = random(4,8);
break;
}
}
}
for (int i = 0; i < 6; 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, 42);
}
// ==================================================
void redBlueWave() {
float t = phase * 0.10;
for (int i = 0; i < NUM_LEDS; i++) {
float wave = sin((i * 0.12) + (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 += 0.6;
if (reveal >= center) {
reveal = 0;
state = (state + 1) % 4;
}
fadeToBlackBy(leds, NUM_LEDS, 1);
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, 3);
float t = phase * 0.3;
for (int i = 0; i < NUM_LEDS; i++) {
if (random8() < 4) leds[i] += CRGB(255,90,0);
}
}
void voidPulse() {
fadeToBlackBy(leds, NUM_LEDS, 10);
float pos = (sin(phase) + 1.0) * 0.5 * NUM_LEDS;
int radius = NUM_LEDS * 0.15;
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 deepColorFade() {
fill_solid(leds, NUM_LEDS, CRGB(200,0,100));
}