Skip to content
254 changes: 254 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata

// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)

#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16))

// static effect, used if an effect fails to initialize
static uint16_t mode_static(void) {
SEGMENT.fill(SEGCOLOR(0));
return strip.isOffRefreshRequired() ? FRAMETIME : 350;
}


/////////////////////////
// User FX functions //
/////////////////////////
Expand Down Expand Up @@ -89,6 +95,253 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW
static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35";


/*
* Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position
* Created by Bob Loeffler and claude.ai
* First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range).
* If value is 0, a random speed will be selected from the full range of values.
* Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range).
* If value is 0, a random time will be selected from the full range of values.
* Third slider (Spinner size) is for the number of pixels that make up the spinner.
* Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin.
* The first checkbox sets the color mode (color wheel or palette).
* The second checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is.
* The third checkbox enables synchronized restart (all spinners restart together instead of individually).
* aux0 stores the settings checksum to detect changes
* aux1 stores the color scale for performance
*/

static uint16_t mode_spinning_wheel(void) {
if (SEGLEN < 1) return mode_static();

unsigned strips = SEGMENT.nrOfVStrips();
if (strips == 0) return mode_static();

constexpr unsigned stateVarsPerStrip = 8;
unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip;
if (!SEGENV.allocateData(dataSize * strips)) return mode_static();
uint32_t* state = reinterpret_cast<uint32_t*>(SEGENV.data);
// state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction)
// state[1] = velocity (fixed point: pixels per frame * 65536)
// state[2] = phase (0=fast spin, 1=slowing, 2=wobble, 3=stopped)
// state[3] = stop time (when phase 3 was entered)
// state[4] = wobble step (0=at stop pos, 1=moved back, 2=returned to stop)
// state[5] = slowdown start time (when to transition from phase 0 to phase 1)
// state[6] = wobble timing (for 200ms / 400ms / 300ms delays)
// state[7] = store the stop position per strip

// state[] index values for easier readability
constexpr unsigned CUR_POS_IDX = 0; // state[0]
constexpr unsigned VELOCITY_IDX = 1;
constexpr unsigned PHASE_IDX = 2;
constexpr unsigned STOP_TIME_IDX = 3;
constexpr unsigned WOBBLE_STEP_IDX = 4;
constexpr unsigned SLOWDOWN_TIME_IDX = 5;
constexpr unsigned WOBBLE_TIME_IDX = 6;
constexpr unsigned STOP_POS_IDX = 7;

SEGMENT.fill(SEGCOLOR(1));

// Handle random seeding globally (outside the virtual strip)
if (SEGENV.call == 0) {
random16_set_seed(hw_random16());
SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling
}

// Check if settings changed (do this once, not per virtual strip)
uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check3;
bool settingsChanged = (SEGENV.aux0 != settingssum);
if (settingsChanged) {
random16_add_entropy(hw_random16());
SEGENV.aux0 = settingssum;
}

// Check if all spinners are stopped and ready to restart (for synchronized restart)
bool allReadyToRestart = true;
if (SEGMENT.check3) {
uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);
uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000);
uint32_t now = strip.now;

for (unsigned stripNr = 0; stripNr < strips; stripNr += spinnerSize) {
uint32_t* stripState = &state[stripNr * stateVarsPerStrip];
// Check if this spinner is stopped AND has waited its delay
if (stripState[PHASE_IDX] != 3 || stripState[STOP_TIME_IDX] == 0) {
allReadyToRestart = false;
break;
}
// Check if delay has elapsed
if ((now - stripState[STOP_TIME_IDX]) < spin_delay) {
allReadyToRestart = false;
break;
}
}
}

struct virtualStrip {
static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart) {

uint8_t phase = state[PHASE_IDX];
uint32_t now = strip.now;

// Check for restart conditions
bool needsReset = false;
if (SEGENV.call == 0) {
needsReset = true;
} else if (settingsChanged) {
needsReset = true;
} else if (phase == 3 && state[STOP_TIME_IDX] != 0) {
// If synchronized restart is enabled, only restart when all strips are ready
if (SEGMENT.check3) {
if (allReadyToRestart) {
needsReset = true;
}
} else {
// Normal mode: restart after individual strip delay
uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000);
if ((now - state[STOP_TIME_IDX]) >= spin_delay) {
needsReset = true;
}
}
}

// Initialize or restart
if (needsReset) {
state[CUR_POS_IDX] = 0;

// Set velocity
uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800);
if (speed == 300) { // random speed (user selected 0 on speed slider)
state[VELOCITY_IDX] = random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100)
} else {
state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655;
}

// Set slowdown start time
uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000);
if (slowdown == 3000) { // random slowdown start time (user selected 0 on intensity slider)
state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000);
} else {
state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000);
}

state[PHASE_IDX] = 0;
state[STOP_TIME_IDX] = 0;
state[WOBBLE_STEP_IDX] = 0;
state[WOBBLE_TIME_IDX] = 0;
state[STOP_POS_IDX] = 0; // Initialize stop position
phase = 0;
}

uint32_t pos_fixed = state[CUR_POS_IDX];
uint32_t velocity = state[VELOCITY_IDX];

// Phase management
if (phase == 0) {
// Fast spinning phase
if ((int32_t)(now - state[SLOWDOWN_TIME_IDX]) >= 0) {
phase = 1;
state[PHASE_IDX] = 1;
}
} else if (phase == 1) {
// Slowing phase - apply deceleration
uint32_t decel = velocity / 80;
if (decel < 100) decel = 100;

velocity = (velocity > decel) ? velocity - decel : 0;
state[VELOCITY_IDX] = velocity;

// Check if stopped
if (velocity < 2000) {
velocity = 0;
state[VELOCITY_IDX] = 0;
phase = 2;
state[PHASE_IDX] = 2;
state[WOBBLE_STEP_IDX] = 0;
uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN;
state[STOP_POS_IDX] = stop_pos;
state[WOBBLE_TIME_IDX] = now;
}
} else if (phase == 2) {
// Wobble phase (moves the LED back one and then forward one)
uint32_t wobble_step = state[WOBBLE_STEP_IDX];
uint16_t stop_pos = state[STOP_POS_IDX];
uint32_t elapsed = now - state[WOBBLE_TIME_IDX];

if (wobble_step == 0 && elapsed >= 200) {
// Move back one LED from stop position
uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1;
pos_fixed = ((uint32_t)back_pos) << 16;
state[CUR_POS_IDX] = pos_fixed;
state[WOBBLE_STEP_IDX] = 1;
state[WOBBLE_TIME_IDX] = now;
} else if (wobble_step == 1 && elapsed >= 400) {
// Move forward to the stop position
pos_fixed = ((uint32_t)stop_pos) << 16;
state[CUR_POS_IDX] = pos_fixed;
state[WOBBLE_STEP_IDX] = 2;
state[WOBBLE_TIME_IDX] = now;
} else if (wobble_step == 2 && elapsed >= 300) {
// Wobble complete, enter stopped phase
phase = 3;
state[PHASE_IDX] = 3;
state[STOP_TIME_IDX] = now;
}
}

// Update position (phases 0 and 1 only)
if (phase == 0 || phase == 1) {
pos_fixed += velocity;
state[CUR_POS_IDX] = pos_fixed;
}

// Draw LED for all phases
uint16_t pos = (pos_fixed >> 16) % SEGLEN;

uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);

// Calculate color once per spinner block (based on strip number, not position)
uint8_t hue;
if (SEGMENT.check2) {
// Each spinner block gets its own color based on strip number
uint16_t numSpinners = max(1U, (SEGMENT.nrOfVStrips() + spinnerSize - 1) / spinnerSize);
hue = (255 * (stripNr / spinnerSize)) / numSpinners;
} else {
// Color changes with position
hue = (SEGENV.aux1 * pos) >> 8;
}

uint32_t color = SEGMENT.check1 ? SEGMENT.color_wheel(hue) : SEGMENT.color_from_palette(hue, true, PALETTE_SOLID_WRAP, 0);

// Draw the spinner with configurable size (1-10 LEDs)
for (int8_t x = 0; x < spinnerSize; x++) {
for (uint8_t y = 0; y < spinnerSize; y++) {
uint16_t drawPos = (pos + y) % SEGLEN;
int16_t drawStrip = stripNr + x;

// Wrap horizontally if needed, or skip if out of bounds
if (drawStrip >= 0 && drawStrip < (int16_t)SEGMENT.nrOfVStrips()) {
SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color);
}
}
}
}
};

for (unsigned stripNr=0; stripNr<strips; stripNr++) {
// Only run on strips that are multiples of spinnerSize to avoid overlap
uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10);
if (stripNr % spinnerSize == 0) {
virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart);
}
}

return FRAMETIME;
}
static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Color mode,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8";



/////////////////////
// UserMod Class //
/////////////////////
Expand All @@ -98,6 +351,7 @@ class UserFxUsermod : public Usermod {
public:
void setup() override {
strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE);
strip.addEffect(255, &mode_spinning_wheel, _data_FX_MODE_SPINNINGWHEEL);

////////////////////////////////////////
// add your effect function(s) here //
Expand Down