diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..bb5e9bd1b5 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -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 // ///////////////////////// @@ -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(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