/* * Amirine Cosplay Lights * * Plays lightshows defined in shows.h on a WS2812B LED strip. * A single button navigates between shows: * 1 tap — next show * 2 taps — previous show * hold — reset to show 0 (blue breath) * * To add or update shows: * 1. Add or edit a .txt file in converter/shows/ * 2. Run: make shows * 3. Run: make upload * * Required library: FastLED (Sketch > Include Library > Manage Libraries) * SPDX-License-Identifier: BSD-2-Clause */ #include "config.h" #include "led_controller.h" #include "button.h" #include "shows.h" // Milliseconds between LED updates. 16ms ≈ 60 refreshes/second. #define UPDATE_INTERVAL_MS 16 // ---- Color math -------------------------------------------------------- // Interpolate a single 8-bit channel from a to b by ratio t (0.0 – 1.0). static uint8_t lerp_channel(uint8_t a, uint8_t b, float t) { return (uint8_t)(a + (b - a) * t); } // Blend two colors: t=0.0 returns 'from', t=1.0 returns 'to'. static CRGB blend_colors(CRGB from, CRGB to, float t) { return CRGB( lerp_channel(from.r, to.r, t), lerp_channel(from.g, to.g, t), lerp_channel(from.b, to.b, t) ); } // ---- Playback state ---------------------------------------------------- static uint8_t s_show_index = 0; // which show is active (index into SHOWS[]) static ShowDef s_show; // active show, read from PROGMEM once on load static uint16_t s_step_index = 0; // current step within the active show static uint32_t s_step_start = 0; // millis() when this step began static CRGB s_from_color = CRGB::Black; // color at the start of this transition // Load show at 'index', resetting playback to the first step. static void load_show(uint8_t index) { s_show_index = index; s_show = read_show_def(&SHOWS[index]); s_step_index = 0; s_step_start = millis(); s_from_color = CRGB::Black; } // How far through the current step we are (0.0 – 1.0). // Returns 1.0 immediately for instant steps (duration_ms == 0). static float step_progress(const Step& step) { if (step.duration_ms == 0) return 1.0f; uint32_t elapsed = millis() - s_step_start; float t = (float)elapsed / step.duration_ms; return (t < 1.0f) ? t : 1.0f; } // Complete the current step and advance to the next. // SHOW_LOOP wraps back to step 0; SHOW_SINGLE loads the next show when the last step ends. static void advance_step(CRGB reached_color) { s_from_color = reached_color; uint16_t next = s_step_index + 1; if (next >= s_show.length) { if (s_show.mode == SHOW_SINGLE) { load_show((s_show_index + 1) % SHOW_COUNT); return; } next = 0; } s_step_index = next; s_step_start = millis(); } // ---- Arduino entry points ---------------------------------------------- void setup() { leds_begin(); button_begin(BUTTON_PIN); load_show(0); // start on show 0 — blue breath } void loop() { // ---- Button navigation ---- ButtonEvent evt = button_update(); if (evt == BTN_TAP) { // Next show, wrapping around to 0 after the last one. load_show((s_show_index + 1) % SHOW_COUNT); } if (evt == BTN_DOUBLE_TAP) { // Previous show, wrapping around to the last one from show 0. load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT); } if (evt == BTN_HOLD) { // Reset to show 0 (blue breath) from any position. load_show(0); } // ---- Playback ---- Step step = read_step(&s_show.steps[s_step_index]); CRGB to = CRGB(step.r, step.g, step.b); float t = step_progress(step); leds_apply_color(blend_colors(s_from_color, to, t)); leds_show(); if (t >= 1.0f) { advance_step(to); } delay(UPDATE_INTERVAL_MS); }