#include <FastLED.h>
#include "config.h" // optional user overrides
// Enable strict diagnostics for this translation unit (our code only)
#pragma GCC diagnostic error "-Wall"
#pragma GCC diagnostic error "-Wextra"
#pragma GCC diagnostic warning "-Wpedantic"
#pragma GCC diagnostic error "-Wpedantic"
#ifndef LED_PIN
#define LED_PIN 2
#endif
#ifndef NUM_LEDS
#define NUM_LEDS 32
#endif
#ifndef BRIGHTNESS
#define BRIGHTNESS 64
#endif
#ifndef LED_TYPE
#define LED_TYPE WS2812B
#endif
#ifndef COLOR_ORDER
#define COLOR_ORDER GRB
#endif
CRGB leds[NUM_LEDS];
#ifndef NUM_DOTS
#define NUM_DOTS 6
#endif
#ifndef SPINNER_COLOR
#define SPINNER_COLOR CRGB::White
#endif
#ifndef SPINNER_BG
#define SPINNER_BG CRGB::Black
#endif
#ifndef INDEX_OFFSET
#define INDEX_OFFSET 0 // rotate starting pixel (0..NUM_LEDS-1)
#endif
#ifndef REVERSE
#define REVERSE 0 // 0 = normal, 1 = reverse direction
#endif
#ifndef SERIAL_BAUD
#define SERIAL_BAUD 2000000
#endif
#ifndef ENABLE_DEBUG
#define ENABLE_DEBUG 0
#endif
#define ANIMATION_PERIOD 2000 // ms per full CSS-like cycle
#define DOT_DELAY_MS 100 // ms per dot delay (CSS: 0.2s)
#define TARGET_FPS 120 // Target frame rate for smooth animation
#define FRAME_TIME_US (1000000UL / TARGET_FPS) // Microseconds per frame
// Dancing direction parameters
#define DANCE_CYCLES 8 // Number of normal cycles before considering direction change
#define REVERSE_CYCLES 3 // Number of cycles to spin in reverse when dancing
#define DANCE_PROBABILITY 30 // Percentage chance (0-100) to start reverse dance
// Segment boundaries (CSS keyframes r: 0%,35%,70%,100%)
#define SEG1_MS ((ANIMATION_PERIOD * 35L) / 100L) // 1750ms
#define SEG2_MS ((ANIMATION_PERIOD * 70L) / 100L) // 3500ms
// Integer-based opacity with fade in/out at ends
static inline uint8_t dot_opacity_ms(uint32_t t_ms)
{
if (t_ms >= SEG2_MS)
{
return 0; // Off after 70%
}
// Define fade zones (10% of the visible period at each end)
uint32_t fade_duration = SEG2_MS / 10; // 10% fade zone
if (t_ms < fade_duration)
{
// Fade in at the beginning (0% to 10%)
return (uint8_t)((uint32_t)t_ms * 255 / fade_duration);
}
else if (t_ms > (SEG2_MS - fade_duration))
{
// Fade out at the end (60% to 70%)
uint32_t fade_pos = SEG2_MS - t_ms;
return (uint8_t)((uint32_t)fade_pos * 255 / fade_duration);
}
else
{
// Full opacity in the middle (10% to 60%)
return 255;
}
}
// Map time within cycle to angular position measured in LED steps with sub-pixel precision.
// CSS r keyframes: -90deg -> 270deg by 35% (one turn), then -> 630deg by 70% (another turn), then hold.
static inline uint32_t pos_steps_ms_fixed(uint32_t t_ms)
{
// Return position in fixed-point format (multiply by 256 for sub-pixel precision)
if (t_ms < SEG1_MS)
{
// First revolution over SEG1_MS
return (uint32_t)t_ms * NUM_LEDS * 256UL / (uint32_t)SEG1_MS; // 0..(NUM_LEDS*256-1)
}
else if (t_ms < SEG2_MS)
{
uint32_t t2 = t_ms - SEG1_MS;
uint32_t seg2_len = SEG2_MS - SEG1_MS; // equals SEG1_MS
return (NUM_LEDS * 256UL) + (t2 * NUM_LEDS * 256UL / seg2_len); // (NUM_LEDS*256)..(2*NUM_LEDS*256-1)
}
else
{
return (uint32_t)(2 * NUM_LEDS * 256UL); // hold after 70%
}
}
void setup()
{
#if ENABLE_DEBUG
Serial.begin(SERIAL_BAUD);
#endif
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(BRIGHTNESS);
}
void loop()
{
static unsigned long last_frame_us = 0;
unsigned long current_us = micros();
// Only update if enough time has passed for the target frame rate
if (current_us - last_frame_us >= FRAME_TIME_US)
{
last_frame_us = current_us;
// Clear all LEDs
fill_solid(leds, NUM_LEDS, SPINNER_BG);
unsigned long now = millis();
uint32_t cycle_ms = (uint32_t)(now % ANIMATION_PERIOD);
// Calculate which animation cycle we're in (for dancing direction)
uint32_t cycle_number = (uint32_t)(now / ANIMATION_PERIOD);
// Determine if we should reverse direction for dancing effect
bool dancing_reverse = false;
uint32_t dance_phase = cycle_number % (DANCE_CYCLES + REVERSE_CYCLES);
// Check if we're in a potential dance period
if (dance_phase >= DANCE_CYCLES)
{
// We're in the reverse dance window
dancing_reverse = true;
}
else if (dance_phase == (DANCE_CYCLES - 1) && cycle_ms == 0)
{
// At the start of the decision cycle, randomly decide to dance
// Use a simple pseudo-random based on cycle number for deterministic behavior
uint32_t pseudo_random = (cycle_number * 31) % 100;
if (pseudo_random < DANCE_PROBABILITY)
{
dancing_reverse = true;
}
}
// Apply dancing reverse on top of the configured REVERSE setting
bool effective_reverse = REVERSE;
if (dancing_reverse)
{
effective_reverse = !effective_reverse;
}
for (uint8_t d = 0; d < NUM_DOTS; d++)
{
// Apply per-dot delay (animation-delay)
uint32_t t_ms = (cycle_ms + ANIMATION_PERIOD - (uint32_t)DOT_DELAY_MS * d) % ANIMATION_PERIOD;
uint8_t opacity = dot_opacity_ms(t_ms);
if (opacity)
{
uint32_t steps_fixed = pos_steps_ms_fixed(t_ms);
uint32_t led_index_fixed = steps_fixed % (NUM_LEDS * 256UL); // base index in fixed-point
// Extract integer and fractional parts
uint8_t led_index = (uint8_t)(led_index_fixed >> 8); // Integer part
uint8_t fraction = (uint8_t)(led_index_fixed & 0xFF); // Fractional part (0-255)
// Apply reverse and offset without floats
uint8_t idx1 = led_index;
if (effective_reverse)
{
idx1 = (uint8_t)((NUM_LEDS - idx1) % NUM_LEDS);
fraction = 255 - fraction; // Reverse the fraction too
}
idx1 = (uint8_t)(idx1 + INDEX_OFFSET);
if (idx1 >= NUM_LEDS)
idx1 = (uint8_t)(idx1 - NUM_LEDS);
// Calculate second LED index for interpolation
uint8_t idx2 = (idx1 + 1) % NUM_LEDS;
// Create rainbow color for each dot
uint8_t hue = (d * 255) / NUM_DOTS; // Distribute hues across rainbow
CRGB c = CHSV(hue, 255, 255); // Full saturation and brightness
// Scale color by opacity
c.r = scale8_video(c.r, opacity);
c.g = scale8_video(c.g, opacity);
c.b = scale8_video(c.b, opacity);
// Interpolate between two adjacent LEDs
if (fraction == 0)
{
// Exactly on LED 1
leds[idx1] += c;
}
else if (fraction == 255)
{
// Exactly on LED 2
leds[idx2] += c;
}
else
{
// Between LEDs - split the color
uint8_t weight1 = 255 - fraction; // Weight for first LED
uint8_t weight2 = fraction; // Weight for second LED
CRGB c1 = c;
CRGB c2 = c;
c1.r = scale8_video(c1.r, weight1);
c1.g = scale8_video(c1.g, weight1);
c1.b = scale8_video(c1.b, weight1);
c2.r = scale8_video(c2.r, weight2);
c2.g = scale8_video(c2.g, weight2);
c2.b = scale8_video(c2.b, weight2);
leds[idx1] += c1; // additive with saturation
leds[idx2] += c2; // additive with saturation
}
}
}
FastLED.show();
}
// No delay - loop runs as fast as possible, but only updates at target FPS
}FPS: 0
Power: 0.00W
Power: 0.00W