Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

// 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 void mode_static(void) {
SEGMENT.fill(SEGCOLOR(0));
Expand Down Expand Up @@ -93,6 +98,251 @@ 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 void mode_spinning_wheel(void) {
if (SEGLEN < 1) FX_FALLBACK_STATIC;

unsigned strips = SEGMENT.nrOfVStrips();
if (strips == 0) FX_FALLBACK_STATIC;

constexpr unsigned stateVarsPerStrip = 8;
unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip;
if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_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);
}
}
}
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 @@ -102,6 +352,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