#include "wled.h" // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata #ifndef WLED_DISABLE_PARTICLESYSTEM1D #include "FXparticleSystem.h" // PS Pendulum requires the 1D particle system #endif // 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)); } #define FX_FALLBACK_STATIC { mode_static(); return; } // If you define configuration options in your class and need to reference them in your effect function, add them here. // If you only need to use them in your class you can define them as class members instead. // bool myConfigValue = false; ///////////////////////// // User FX functions // ///////////////////////// // Diffusion Fire: fire effect intended for 2D setups smaller than 16x16 static void mode_diffusionfire(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; const auto XY = [&](int x, int y) { return x + y * cols; }; const uint8_t refresh_hz = map(SEGMENT.speed, 0, 255, 20, 80); const unsigned refresh_ms = 1000 / refresh_hz; const int16_t diffusion = map(SEGMENT.custom1, 0, 255, 0, 100); const uint8_t spark_rate = SEGMENT.intensity; const uint8_t turbulence = SEGMENT.custom2; unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // allocation failed if (SEGENV.call == 0) { SEGMENT.fill(BLACK); SEGENV.step = 0; } if ((strip.now - SEGENV.step) >= refresh_ms) { // Keep for ≤~1 KiB; otherwise consider heap or reuse SEGENV.data as scratch. uint8_t tmp_row[cols]; SEGENV.step = strip.now; // scroll up for (unsigned y = 1; y < rows; y++) for (unsigned x = 0; x < cols; x++) { unsigned src = XY(x, y); unsigned dst = XY(x, y - 1); SEGENV.data[dst] = SEGENV.data[src]; } if (hw_random8() > turbulence) { // create new sparks at bottom row for (unsigned x = 0; x < cols; x++) { uint8_t p = hw_random8(); if (p < spark_rate) { unsigned dst = XY(x, rows - 1); SEGENV.data[dst] = 255; } } } // diffuse for (unsigned y = 0; y < rows; y++) { for (unsigned x = 0; x < cols; x++) { unsigned v = SEGENV.data[XY(x, y)]; if (x > 0) { v += SEGENV.data[XY(x - 1, y)]; } if (x < (cols - 1)) { v += SEGENV.data[XY(x + 1, y)]; } tmp_row[x] = min(255, (int)(v * 100 / (300 + diffusion))); } for (unsigned x = 0; x < cols; x++) { SEGENV.data[XY(x, y)] = tmp_row[x]; if (SEGMENT.check1) { uint32_t color = SEGMENT.color_from_palette(tmp_row[x], true, false, 0); SEGMENT.setPixelColorXY(x, y, color); } else { uint32_t base = SEGCOLOR(0); SEGMENT.setPixelColorXY(x, y, color_fade(base, tmp_row[x])); } } } } } 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 allows the spinner to spin. If it's enabled, the spinner will do its thing. If it's not enabled, it will wait for the user to enable * it either by clicking the checkbox or by pressing a physical button (e.g. using a playlist to run a couple presets that have JSON API codes). * 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(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) { 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.check1 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { 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, unsigned strips) { 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 && SEGMENT.check1) { 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 && SEGMENT.check1) { // spin the spinner(s) only if the "Spin me!" checkbox is enabled 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] = hw_random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) } else { state[VELOCITY_IDX] = hw_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 + hw_random16(2000, 6000); } else { state[SLOWDOWN_TIME_IDX] = now + hw_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, (strips + spinnerSize - 1) / spinnerSize); hue = (uint32_t)(255) * (stripNr / spinnerSize) / numSpinners; } else { // Color changes with position hue = (SEGENV.aux1 * pos) >> 8; } uint32_t color = ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND); // 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 < strips) { 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, strips); } } } static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o1=1,o3=1"; /* / Lava Lamp 2D effect * Uses particles to simulate rising blobs of "lava" or wax * Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again * Created by Bob Loeffler using claude.ai * The first slider sets the number of active blobs * The second slider sets the size range of the blobs * The third slider sets the damping value for horizontal blob movement * The Attract checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally) * The Keep Color Ratio checkbox sets whether we preserve the color ratio when displaying pixels that are in 2 or more overlapping blobs * aux0 keeps track of the blob size value * aux1 keeps track of the number of blobs */ typedef struct LavaParticle { float x, y; // Position float vx, vy; // Velocity float size; // Blob size uint8_t hue; // Color bool active; // will not be displayed if false uint16_t delayTop; // number of frames to wait at top before falling again bool idleTop; // sitting idle at the top uint16_t delayBottom; // number of frames to wait at bottom before rising again bool idleBottom; // sitting idle at the bottom } LavaParticle; static void mode_2D_lavalamp(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t cols = SEG_W; const uint16_t rows = SEG_H; constexpr float MAX_BLOB_RADIUS = 20.0f; // cap to prevent frame rate drops on large matrices constexpr size_t MAX_LAVA_PARTICLES = 34; // increasing this value could cause slowness for large matrices constexpr size_t MAX_TOP_FPS_DELAY = 900; // max delay when particles are at the top constexpr size_t MAX_BOTTOM_FPS_DELAY = 1200; // max delay when particles are at the bottom // Allocate per-segment storage if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) FX_FALLBACK_STATIC; LavaParticle* lavaParticles = reinterpret_cast(SEGENV.data); // Initialize particles on first call if (SEGENV.call == 0) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { lavaParticles[i].active = false; } } // Track particle size and particle count slider changes, re-initialize if either changes uint8_t currentNumParticles = (SEGMENT.intensity >> 3) + 3; uint8_t currentSize = SEGMENT.custom1; if (currentNumParticles > MAX_LAVA_PARTICLES) currentNumParticles = MAX_LAVA_PARTICLES; bool needsReinit = (currentSize != SEGENV.aux0) || (currentNumParticles != SEGENV.aux1); if (needsReinit) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { lavaParticles[i].active = false; } SEGENV.aux0 = currentSize; SEGENV.aux1 = currentNumParticles; } uint8_t size = currentSize; uint8_t numParticles = currentNumParticles; // blob size based on matrix width const float minSize = cols * 0.15f; // Minimum 15% of width const float maxSize = cols * 0.4f; // Maximum 40% of width float sizeRange = (maxSize - minSize) * (size / 255.0f); int rangeInt = max(1, (int)(sizeRange)); // calculate the spawning area for the particles const float spawnXStart = cols * 0.20f; const float spawnXWidth = cols * 0.60f; int spawnX = max(1, (int)(spawnXWidth)); bool preserveColorRatio = SEGMENT.check3; // Spawn new particles at the bottom near the center for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active && hw_random8() < 32) { // spawn when slot available // Spawn in the middle 60% of the matrix width lavaParticles[i].x = spawnXStart + (float)hw_random16(spawnX); lavaParticles[i].y = rows - 1; lavaParticles[i].vx = (hw_random16(7) - 3) / 250.0f; lavaParticles[i].vy = -(hw_random16(20) + 10) / 100.0f * 0.3f; lavaParticles[i].size = minSize + (float)hw_random16(rangeInt); if (lavaParticles[i].size > MAX_BLOB_RADIUS) lavaParticles[i].size = MAX_BLOB_RADIUS; lavaParticles[i].hue = hw_random8(); lavaParticles[i].active = true; // Set random delays when particles are at top and bottom lavaParticles[i].delayTop = hw_random16(MAX_TOP_FPS_DELAY); lavaParticles[i].delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY); lavaParticles[i].idleBottom = true; break; } } // Fade background slightly for trailing effect SEGMENT.fadeToBlackBy(40); // Update and draw particles int activeCount = 0; unsigned long currentMillis = strip.now; for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active) continue; activeCount++; // Keep particle count on target by deactivating excess particles if (activeCount > numParticles) { lavaParticles[i].active = false; activeCount--; continue; } LavaParticle *p = &lavaParticles[i]; // Physics update p->x += p->vx; p->y += p->vy; // Optional particle/blob attraction if (SEGMENT.check2) { for (int j = 0; j < MAX_LAVA_PARTICLES; j++) { if (i == j || !lavaParticles[j].active) continue; LavaParticle *other = &lavaParticles[j]; // Skip attraction if moving in same vertical direction (both up or both down) if ((p->vy < 0 && other->vy < 0) || (p->vy > 0 && other->vy > 0)) continue; float dx = other->x - p->x; float dy = other->y - p->y; // Apply weak horizontal attraction only float attractRange = p->size + other->size; float distSq = dx*dx + dy*dy; float attractRangeSq = attractRange * attractRange; if (distSq > 0 && distSq < attractRangeSq) { float dist = sqrtf(distSq); // Only compute sqrt when needed float force = (1.0f - (dist / attractRange)) * 0.0001f; p->vx += (dx / dist) * force; } } } // Horizontal oscillation (makes it more organic) float damping= map(SEGMENT.custom2, 0, 255, 97, 87) / 100.0f; p->vx += sin((currentMillis / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation p->vx *= damping; // damping for more or less horizontal drift // Bounce off sides (don't affect vertical velocity) if (p->x < 0) { p->x = 0; p->vx = abs(p->vx); // reverse horizontal } if (p->x >= cols) { p->x = cols - 1; p->vx = -abs(p->vx); // reverse horizontal } // Adjust rise/fall velocity depending on approx distance from heat source (at bottom) // In top 1/4th of rows... if (p->y < rows * .25f) { if (p->vy >= 0) { // if going down, delay the particles so they won't go down immediately if (p->delayTop > 0 && p->idleTop) { p->vy = 0.0f; p->delayTop--; p->idleTop = true; } else { p->vy = 0.01f; p->delayTop = hw_random16(MAX_TOP_FPS_DELAY); p->idleTop = false; } } else if (p->vy <= 0) { // if going up, slow down the rise rate p->vy = -0.03f; } } // In next 1/4th of rows... if (p->y <= rows * .50f && p->y >= rows * .25f) { if (p->vy > 0) { // if going down, speed up the fall rate p->vy = 0.03f; } else if (p->vy <= 0) { // if going up, speed up the rise rate a little more p->vy = -0.05f; } } // In next 1/4th of rows... if (p->y <= rows * .75f && p->y >= rows * .50f) { if (p->vy > 0) { // if going down, speed up the fall rate a little more p->vy = 0.04f; } else if (p->vy <= 0) { // if going up, speed up the rise rate p->vy = -0.03f; } } // In bottom 1/4th of rows... if (p->y > rows * .75f) { if (p->vy >= 0) { // if going down, slow down the fall rate p->vy = 0.02f; } else if (p->vy <= 0) { // if going up, delay the particles so they won't go up immediately if (p->delayBottom > 0 && p->idleBottom) { p->vy = 0.0f; p->delayBottom--; p->idleBottom = true; } else { p->vy = -0.01f; p->delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY); p->idleBottom = false; } } } // Boundary handling with reversal of direction // When reaching TOP (y=0 area), reverse to fall back down, but need to delay first if (p->y <= 0.5f * p->size) { p->y = 0.5f * p->size; if (p->vy < 0) { p->vy = 0.005f; // set to a tiny positive value to start falling very slowly p->idleTop = true; } } // When reaching BOTTOM (y=rows-1 area), reverse to rise back up, but need to delay first if (p->y >= rows - 0.5f * p->size) { p->y = rows - 0.5f * p->size; if (p->vy > 0) { p->vy = -0.005f; // set to a tiny negative value to start rising very slowly p->idleBottom = true; } } // Get color uint32_t color; color = SEGMENT.color_from_palette(p->hue, true, PALETTE_SOLID_WRAP, 0); // Extract RGB and apply life/opacity uint8_t w = (W(color) * 255) >> 8; uint8_t r = (R(color) * 255) >> 8; uint8_t g = (G(color) * 255) >> 8; uint8_t b = (B(color) * 255) >> 8; // Draw blob with sub-pixel accuracy using bilinear distribution float sizeSq = p->size * p->size; // Get fractional offsets of particle center float fracX = p->x - floorf(p->x); float fracY = p->y - floorf(p->y); int centerX = (int)floorf(p->x); int centerY = (int)floorf(p->y); for (int dy = -(int)p->size - 1; dy <= (int)p->size + 1; dy++) { for (int dx = -(int)p->size - 1; dx <= (int)p->size + 1; dx++) { int px = centerX + dx; int py = centerY + dy; if (px < 0 || px >= cols || py < 0 || py >= rows) continue; // Sub-pixel distance: measure from true float center to pixel center float subDx = dx - fracX; // distance from true center to this pixel's center float subDy = dy - fracY; float distSq = subDx * subDx + subDy * subDy; if (distSq < sizeSq) { float intensity = 1.0f - (distSq / sizeSq); intensity = intensity * intensity; // smooth falloff uint8_t bw = (uint8_t)(w * intensity); uint8_t br = (uint8_t)(r * intensity); uint8_t bg = (uint8_t)(g * intensity); uint8_t bb = (uint8_t)(b * intensity); uint32_t existing = SEGMENT.getPixelColorXY(px, py); uint32_t newColor = RGBW32(br, bg, bb, bw); SEGMENT.setPixelColorXY(px, py, color_add(existing, newColor, preserveColorRatio ? true : false)); } } } } } static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@,# of blobs,Blob size,H. Damping,,,Attract,Keep Color Ratio;;!;2;ix=64,c2=192,o2=1,o3=1,pal=47"; /* / Magma effect * 2D magma/lava animation * Adapted from FireLamp_JeeUI implementation (https://github.com/DmytroKorniienko/FireLamp_JeeUI/tree/dev) * Original idea by SottNick, remastered by kostyamat * Adapted to WLED by Bob Loeffler and claude.ai * First slider (speed) is for the speed or flow rate of the moving magma. * Second slider (intensity) is for the height of the magma. * Third slider (lava bombs) is for the number of lava bombs (particles). The max # is 1/2 the number of columns on the 2D matrix. * Fourth slider (gravity) is for how high the lava bombs will go. * The checkbox (check2) is for whether the lava bombs can be seen in the magma or behind it. */ // Draw the magma static void drawMagma(const uint16_t width, const uint16_t height, float *ff_y, float *ff_z, uint8_t *shiftHue) { // Noise parameters - adjust these for different magma characteristics // deltaValue: higher = more detailed/turbulent magma // deltaHue: higher = taller magma structures constexpr uint8_t magmaDeltaValue = 12U; constexpr uint8_t magmaDeltaHue = 10U; uint16_t ff_y_int = (uint16_t)*ff_y; uint16_t ff_z_int = (uint16_t)*ff_z; for (uint16_t i = 0; i < width; i++) { for (uint16_t j = 0; j < height; j++) { // Generate Perlin noise value (0-255) uint8_t noise = perlin8(i * magmaDeltaValue, (j + ff_y_int + hw_random8(2)) * magmaDeltaHue, ff_z_int); uint8_t paletteIndex = qsub8(noise, shiftHue[j]); // Apply the vertical fade gradient CRGB col = SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, 0); // Get color from palette SEGMENT.addPixelColorXY(i, height - 1 - j, col); // magma rises from bottom of display } } } // Move and draw lava bombs (particles) static void drawLavaBombs(const uint16_t width, const uint16_t height, float *particleData, float gravity, uint8_t particleCount) { for (uint16_t i = 0; i < particleCount; i++) { uint16_t idx = i * 4; particleData[idx + 3] -= gravity; particleData[idx + 0] += particleData[idx + 2]; particleData[idx + 1] += particleData[idx + 3]; float posX = particleData[idx + 0]; float posY = particleData[idx + 1]; if (posY > height + height / 4) { particleData[idx + 3] = -particleData[idx + 3] * 0.8f; } if (posY < (float)(height / 8) - 1.0f || posX < 0 || posX >= width) { particleData[idx + 0] = hw_random(0, width * 100) / 100.0f; particleData[idx + 1] = hw_random(0, height * 25) / 100.0f; particleData[idx + 2] = hw_random(-75, 75) / 100.0f; float baseVelocity = hw_random(60, 120) / 100.0f; if (hw_random8() < 50) { baseVelocity *= 1.6f; } particleData[idx + 3] = baseVelocity; continue; } int16_t xi = (int16_t)posX; int16_t yi = (int16_t)posY; if (xi >= 0 && xi < width && yi >= 0 && yi < height) { // Get a random color from the current palette uint8_t randomIndex = hw_random8(64, 128); CRGB pcolor = ColorFromPalette(SEGPALETTE, randomIndex, 255, LINEARBLEND); // Pre-calculate anti-aliasing weights float xf = posX - xi; float yf = posY - yi; float ix = 1.0f - xf; float iy = 1.0f - yf; uint8_t w0 = 255 * ix * iy; uint8_t w1 = 255 * xf * iy; uint8_t w2 = 255 * ix * yf; uint8_t w3 = 255 * xf * yf; int16_t yFlipped = height - 1 - yi; // Flip Y coordinate SEGMENT.addPixelColorXY(xi, yFlipped, pcolor.scale8(w0)); if (xi + 1 < width) SEGMENT.addPixelColorXY(xi + 1, yFlipped, pcolor.scale8(w1)); if (yFlipped - 1 >= 0) SEGMENT.addPixelColorXY(xi, yFlipped - 1, pcolor.scale8(w2)); if (xi + 1 < width && yFlipped - 1 >= 0) SEGMENT.addPixelColorXY(xi + 1, yFlipped - 1, pcolor.scale8(w3)); } } } static void mode_2D_magma(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t width = SEG_W; const uint16_t height = SEG_H; const uint8_t MAGMA_MAX_PARTICLES = width / 2; if (MAGMA_MAX_PARTICLES < 2) FX_FALLBACK_STATIC; // matrix too narrow for lava bombs constexpr size_t SETTINGS_SUM_BYTES = 4; // 4 bytes for settings sum // Allocate memory: particles (4 floats each) + 2 floats for noise counters + shiftHue cache + settingsSum const uint16_t dataSize = (MAGMA_MAX_PARTICLES * 4 + 2) * sizeof(float) + height * sizeof(uint8_t) + SETTINGS_SUM_BYTES; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // allocation failed float* particleData = reinterpret_cast(SEGENV.data); float* ff_y = &particleData[MAGMA_MAX_PARTICLES * 4]; float* ff_z = &particleData[MAGMA_MAX_PARTICLES * 4 + 1]; uint32_t* settingsSumPtr = reinterpret_cast(&particleData[MAGMA_MAX_PARTICLES * 4 + 2]); uint8_t* shiftHue = reinterpret_cast(reinterpret_cast(settingsSumPtr) + SETTINGS_SUM_BYTES); // Check if settings changed uint32_t settingsKey = (uint32_t)SEGMENT.speed | ((uint32_t)SEGMENT.intensity << 8) | ((uint32_t)SEGMENT.custom1 << 16) | ((uint32_t)SEGMENT.custom2 << 24); bool settingsChanged = (*settingsSumPtr != settingsKey); if (SEGENV.call == 0 || settingsChanged) { // Intensity slider controls magma height uint16_t intensity = SEGMENT.intensity; uint16_t fadeRange = map(intensity, 0, 255, height / 3, height); // shiftHue controls the vertical color gradient (magma fades out toward top) for (uint16_t j = 0; j < height; j++) { if (j < fadeRange) { // prevent division issues and ensure smooth gradient if (fadeRange > 1) { shiftHue[j] = (uint8_t)(j * 255 / (fadeRange - 1)); } else { shiftHue[j] = 0; // Single row magma = no fade } } else { shiftHue[j] = 255; } } // Initialize all particles for (uint16_t i = 0; i < MAGMA_MAX_PARTICLES; i++) { uint16_t idx = i * 4; particleData[idx + 0] = hw_random(0, width * 100) / 100.0f; particleData[idx + 1] = hw_random(0, height * 25) / 100.0f; particleData[idx + 2] = hw_random(-75, 75) / 100.0f; float baseVelocity = hw_random(60, 120) / 100.0f; if (hw_random8() < 50) { baseVelocity *= 1.6f; } particleData[idx + 3] = baseVelocity; } *ff_y = 0.0f; *ff_z = 0.0f; *settingsSumPtr = settingsKey; } if (!shiftHue) FX_FALLBACK_STATIC; // safety check // Speed control float speedfactor = SEGMENT.speed / 255.0f; speedfactor = speedfactor * speedfactor * 1.5f; if (speedfactor < 0.001f) speedfactor = 0.001f; // Gravity control float gravity = map(SEGMENT.custom2, 0, 255, 5, 20) / 100.0f; // Number of particles (lava bombs) uint8_t particleCount = map(SEGMENT.custom1, 0, 255, 0, MAGMA_MAX_PARTICLES); particleCount = constrain(particleCount, 0, MAGMA_MAX_PARTICLES); // Draw lava bombs in front of magma (or behind it) if (SEGMENT.check2) { drawMagma(width, height, ff_y, ff_z, shiftHue); SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount); } else { if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount); SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect drawMagma(width, height, ff_y, ff_z, shiftHue); } // noise counters based on speed slider *ff_y += speedfactor * 2.0f; *ff_z += speedfactor; SEGENV.step++; } static const char _data_FX_MODE_2D_MAGMA[] PROGMEM = "Magma@Flow rate,Magma height,Lava bombs,Gravity,,,Bombs in front;;!;2;ix=192,c2=32,o2=1,pal=35"; /* / Ants (created by making modifications to the Rolling Balls code) - Bob Loeffler 2025 * First slider is for the ants' speed. * Second slider is for the # of ants. * Third slider is for the Ants' size. * Fourth slider (custom2) is for blurring the LEDs in the segment. * Checkbox1 is for Gathering food (enabled if you want the ants to gather food, disabled if they are just walking). * We will switch directions when they get to the beginning or end of the segment when gathering food. * When gathering food, the Pass By option will automatically be enabled so they can drop off their food easier (and look for more food). * Checkbox2 is for Smear mode (enabled is smear pixel colors, disabled is no smearing) * Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled) */ // Ant structure representing each ant's state struct Ant { unsigned long lastBumpUpdate; // the last time the ant bumped into another ant bool hasFood; float velocity; float position; // (0.0 to 1.0 range) }; constexpr unsigned MAX_ANTS = 32; constexpr float MIN_COLLISION_TIME_MS = 2.0f; constexpr float VELOCITY_MIN = 2.0f; constexpr float VELOCITY_MAX = 10.0f; constexpr unsigned ANT_SIZE_MIN = 1; constexpr unsigned ANT_SIZE_MAX = 20; // Helper function to get food pixel color based on ant and background colors static uint32_t getFoodColor(uint32_t antColor, uint32_t backgroundColor) { if (antColor == WHITE) return (backgroundColor == YELLOW) ? GRAY : YELLOW; return (backgroundColor == WHITE) ? YELLOW : WHITE; } // Helper function to handle ant boundary wrapping or bouncing static void handleBoundary(Ant& ant, float& position, bool gatherFood, bool atStart, unsigned long currentTime) { if (gatherFood) { // Bounce mode: reverse direction and update food status position = atStart ? 0.0f : 1.0f; ant.velocity = -ant.velocity; ant.lastBumpUpdate = currentTime; ant.position = position; ant.hasFood = atStart; // Has food when leaving start, drops it at end } else { // Wrap mode: teleport to opposite end position = atStart ? 1.0f : 0.0f; ant.lastBumpUpdate = currentTime; ant.position = position; } } // Helper function to calculate ant color static uint32_t getAntColor(int antIndex, int numAnts, bool usePalette) { if (usePalette) return SEGMENT.color_from_palette(antIndex * 255 / numAnts, false, (paletteBlend == 1 || paletteBlend == 3), 255); // Alternate between two colors for default palette return (antIndex % 3 == 1) ? SEGCOLOR(0) : SEGCOLOR(2); } // Helper function to render a single ant pixel with food handling static void renderAntPixel(int pixelIndex, int pixelOffset, int antSize, const Ant& ant, uint32_t antColor, uint32_t backgroundColor, bool gatherFood) { bool isMovingBackward = (ant.velocity < 0); bool isFoodPixel = gatherFood && ant.hasFood && ((isMovingBackward && pixelOffset == 0) || (!isMovingBackward && pixelOffset == antSize - 1)); if (isFoodPixel) { SEGMENT.setPixelColor(pixelIndex, getFoodColor(antColor, backgroundColor)); } else { SEGMENT.setPixelColor(pixelIndex, antColor); } } static void mode_ants(void) { if (SEGLEN <= 1) FX_FALLBACK_STATIC; // Allocate memory for ant data uint32_t backgroundColor = SEGCOLOR(1); unsigned dataSize = sizeof(Ant) * MAX_ANTS; if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; // Allocation failed Ant* ants = reinterpret_cast(SEGENV.data); // Extract configuration from segment settings unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS); bool gatherFood = SEGMENT.check1; bool SmearMode = SEGMENT.check2; bool passBy = SEGMENT.check3 || gatherFood; // global no‑collision when gathering food is enabled unsigned antSize = map(SEGMENT.custom1, 0, 255, ANT_SIZE_MIN, ANT_SIZE_MAX) + (gatherFood ? 1 : 0); // Initialize ants on first call if (SEGENV.call == 0) { int confusedAntIndex = hw_random(0, numAnts); // the first random ant to go backwards for (int i = 0; i < MAX_ANTS; i++) { ants[i].lastBumpUpdate = strip.now; // Random velocity float velocity = VELOCITY_MIN + (VELOCITY_MAX - VELOCITY_MIN) * hw_random16(1000, 5000) / 5000.0f; // One random ant moves in opposite direction ants[i].velocity = (i == confusedAntIndex) ? -velocity : velocity; // Random starting position (0.0 to 1.0) ants[i].position = hw_random16(0, 10000) / 10000.0f; // Ants don't have food yet ants[i].hasFood = false; } } // Calculate time conversion factor based on speed slider float timeConversionFactor = float(scale8(8, 255 - SEGMENT.speed) + 1) * 20000.0f; // Clear background if not in Smear mode if (!SmearMode) SEGMENT.fill(backgroundColor); // Update and render each ant for (int i = 0; i < numAnts; i++) { float timeSinceLastUpdate = float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; float newPosition = ants[i].position + ants[i].velocity * timeSinceLastUpdate; // Reset ants that wandered too far off-track (e.g., after intensity change) if (newPosition < -0.5f || newPosition > 1.5f) { newPosition = ants[i].position = hw_random16(0, 10000) / 10000.0f; ants[i].lastBumpUpdate = strip.now; } // Handle boundary conditions (bounce or wrap) if (newPosition <= 0.0f && ants[i].velocity < 0.0f) { handleBoundary(ants[i], newPosition, gatherFood, true, strip.now); } else if (newPosition >= 1.0f && ants[i].velocity > 0.0f) { handleBoundary(ants[i], newPosition, gatherFood, false, strip.now); } // Handle collisions between ants (if not passing by) if (!passBy) { for (int j = i + 1; j < numAnts; j++) { if (fabsf(ants[j].velocity - ants[i].velocity) < 0.001f) continue; // Moving in same direction at same speed; avoids tiny denominators // Calculate collision time using physics - collisionTime formula adapted from rolling_balls float timeOffset = float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate)); float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity); // Check if collision occurred in valid time window float timeSinceJ = float(int(strip.now - ants[j].lastBumpUpdate)); if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) { // Update positions to collision point float adjustedTime = (collisionTime + float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate))) / timeConversionFactor; ants[i].position += ants[i].velocity * adjustedTime; ants[j].position = ants[i].position; // Update collision time unsigned long collisionMoment = static_cast(collisionTime + 0.5f) + ants[j].lastBumpUpdate; ants[i].lastBumpUpdate = collisionMoment; ants[j].lastBumpUpdate = collisionMoment; // Reverse the ant with greater speed magnitude if (fabsf(ants[i].velocity) > fabsf(ants[j].velocity)) { ants[i].velocity = -ants[i].velocity; } else { ants[j].velocity = -ants[j].velocity; } // Recalculate position after collision newPosition = ants[i].position + ants[i].velocity * float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; } } } // Clamp position to valid range newPosition = constrain(newPosition, 0.0f, 1.0f); unsigned pixelPosition = roundf(newPosition * (SEGLEN - 1)); // Determine ant color uint32_t antColor = getAntColor(i, numAnts, SEGMENT.palette != 0); // Render ant pixels for (int pixelOffset = 0; pixelOffset < antSize; pixelOffset++) { unsigned currentPixel = pixelPosition + pixelOffset; if (currentPixel >= SEGLEN) break; renderAntPixel(currentPixel, pixelOffset, antSize, ants[i], antColor, backgroundColor, gatherFood); } // Update ant state ants[i].lastBumpUpdate = strip.now; ants[i].position = newPosition; } SEGMENT.blur(SEGMENT.custom2>>1); } static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; /* / Morse Code by Bob Loeffler * Adapted from code by automaticaddison.com and then optimized by claude.ai * aux0 is the pattern offset for scrolling * aux1 saves settings: check2 (1 bit), check3 (1 bit), text hash (4 bits) and pattern length (10 bits) * The first slider (sx) selects the scrolling speed * The second slider selects the color mode (lower half selects color wheel, upper half selects color palettes) * Checkbox1 displays all letters in a word with the same color * Checkbox2 displays punctuation or not * Checkbox3 displays the End-of-message code or not * We get the text from the SEGMENT.name and convert it to morse code * This effect uses a bit array, instead of bool array, for efficient storage - 8x memory reduction (128 bytes vs 1024 bytes) * * Morse Code rules: * - a dot is 1 pixel/LED; a dash is 3 pixels/LEDs * - there is 1 space between each dot or dash that make up a letter/number/punctuation * - there are 3 spaces between each letter/number/punctuation * - there are 7 spaces between each word */ // Bit manipulation macros #define SET_BIT8(arr, i) ((arr)[(i) >> 3] |= (1 << ((i) & 7))) #define GET_BIT8(arr, i) (((arr)[(i) >> 3] & (1 << ((i) & 7))) != 0) // Build morse code pattern into a buffer static void build_morsecode_pattern(const char *morse_code, uint8_t *pattern, uint8_t *wordIndex, uint16_t &index, uint8_t currentWord, int maxSize) { const char *c = morse_code; // Build the dots and dashes into pattern array while (*c != '\0') { // it's a dot which is 1 pixel if (*c == '.') { if (index >= maxSize - 1) return; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; } else { // Must be a dash which is 3 pixels if (index >= maxSize - 3) return; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; SET_BIT8(pattern, index); wordIndex[index] = currentWord; index++; } c++; // 1 space between parts of a letter/number/punctuation (but not after the last one) if (*c != '\0') { if (index >= maxSize) return; wordIndex[index] = currentWord; index++; } } // 3 spaces between two letters/numbers/punctuation if (index >= maxSize - 2) return; wordIndex[index] = currentWord; index++; if (index >= maxSize - 1) return; wordIndex[index] = currentWord; index++; if (index >= maxSize) return; wordIndex[index] = currentWord; index++; } static void mode_morsecode(void) { if (SEGLEN < 1) FX_FALLBACK_STATIC; // A-Z in Morse Code static const char * letters[] = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--.."}; // 0-9 in Morse Code static const char * numbers[] = {"-----", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----."}; // Punctuation in Morse Code struct PunctuationMapping { char character; const char* code; }; static const PunctuationMapping punctuation[] = { {'.', ".-.-.-"}, {',', "--..--"}, {'?', "..--.."}, {':', "---..."}, {'-', "-....-"}, {'!', "-.-.--"}, {'&', ".-..."}, {'@', ".--.-."}, {')', "-.--.-"}, {'(', "-.--."}, {'/', "-..-."}, {'\'', ".----."} }; // Get the text to display char text[WLED_MAX_SEGNAME_LEN+1] = {'\0'}; size_t len = 0; if (SEGMENT.name) len = strlen(SEGMENT.name); if (len == 0) { strcpy_P(text, PSTR("I Love WLED!")); } else { strcpy(text, SEGMENT.name); } // Convert to uppercase in place for (char *p = text; *p; p++) { *p = toupper(*p); } // Allocate per-segment storage for pattern (1023 bits = 127 bytes) + word index array (1024 bytes) + word count (1 byte) constexpr size_t MORSECODE_MAX_PATTERN_SIZE = 1023; constexpr size_t MORSECODE_PATTERN_BYTES = (MORSECODE_MAX_PATTERN_SIZE + 7) / 8; // 128 bytes constexpr size_t MORSECODE_WORD_INDEX_BYTES = MORSECODE_MAX_PATTERN_SIZE; // 1 byte per bit position constexpr size_t MORSECODE_WORD_COUNT_BYTES = 1; // 1 byte for word count if (!SEGENV.allocateData(MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES + MORSECODE_WORD_COUNT_BYTES)) FX_FALLBACK_STATIC; uint8_t* morsecodePattern = reinterpret_cast(SEGENV.data); uint8_t* wordIndexArray = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES); uint8_t* wordCountPtr = reinterpret_cast(SEGENV.data + MORSECODE_PATTERN_BYTES + MORSECODE_WORD_INDEX_BYTES); // SEGENV.aux1 stores: [bit 15: check2] [bit 14: check3] [bits 10-13: text hash (4 bits)] [bits 0-9: pattern length] bool lastCheck2 = (SEGENV.aux1 & 0x8000) != 0; bool lastCheck3 = (SEGENV.aux1 & 0x4000) != 0; uint16_t lastHashBits = (SEGENV.aux1 >> 10) & 0xF; // 4 bits of hash uint16_t patternLength = SEGENV.aux1 & 0x3FF; // Lower 10 bits for length (up to 1023) // Compute text hash uint16_t textHash = 0; for (char *p = text; *p; p++) { textHash = ((textHash << 5) + textHash) + *p; } uint16_t currentHashBits = (textHash >> 12) & 0xF; // Use upper 4 bits of hash bool textChanged = (currentHashBits != lastHashBits) && (SEGENV.call > 0); // Check if we need to rebuild the pattern bool needsRebuild = (SEGENV.call == 0) || textChanged || (SEGMENT.check2 != lastCheck2) || (SEGMENT.check3 != lastCheck3); // Initialize on first call or rebuild pattern if (needsRebuild) { patternLength = 0; // Clear the bit array and word index array first memset(morsecodePattern, 0, MORSECODE_PATTERN_BYTES); memset(wordIndexArray, 0, MORSECODE_WORD_INDEX_BYTES); // Track current word index uint8_t currentWordIndex = 0; // Build complete morse code pattern for (char *c = text; *c; c++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE - 10) break; if (*c >= 'A' && *c <= 'Z') { build_morsecode_pattern(letters[*c - 'A'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } else if (*c >= '0' && *c <= '9') { build_morsecode_pattern(numbers[*c - '0'], morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } else if (*c == ' ') { // Space between words - increment word index for next word currentWordIndex++; // Add 4 additional spaces (7 total with the 3 after each letter) for (int x = 0; x < 4; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; wordIndexArray[patternLength] = currentWordIndex; patternLength++; } } else if (SEGMENT.check2) { const char *punctuationCode = nullptr; for (const auto& p : punctuation) { if (*c == p.character) { punctuationCode = p.code; break; } } if (punctuationCode) { build_morsecode_pattern(punctuationCode, morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } } } if (SEGMENT.check3) { build_morsecode_pattern(".-.-.", morsecodePattern, wordIndexArray, patternLength, currentWordIndex, MORSECODE_MAX_PATTERN_SIZE); } for (int x = 0; x < 7; x++) { if (patternLength >= MORSECODE_MAX_PATTERN_SIZE) break; wordIndexArray[patternLength] = currentWordIndex; patternLength++; } // Store the total number of words (currentWordIndex + 1 because it's 0-indexed) *wordCountPtr = currentWordIndex + 1; // Store pattern length, checkbox states, and hash bits in aux1 SEGENV.aux1 = patternLength | (currentHashBits << 10) | (SEGMENT.check2 ? 0x8000 : 0) | (SEGMENT.check3 ? 0x4000 : 0); // Reset the scroll offset SEGENV.aux0 = 0; } // if pattern is empty for some reason, display black background only if (patternLength == 0) { SEGMENT.fill(BLACK); return; } // Update offset to make the morse code scroll // Use step for scroll timing only uint32_t cycleTime = 50 + (255 - SEGMENT.speed)*3; uint32_t it = strip.now / cycleTime; if (SEGENV.step != it) { SEGENV.aux0++; SEGENV.step = it; } // Clear background SEGMENT.fill(BLACK); // Draw the scrolling pattern int offset = SEGENV.aux0 % patternLength; // Get the word count and calculate color spacing uint8_t wordCount = *wordCountPtr; if (wordCount == 0) wordCount = 1; uint8_t colorSpacing = 255 / wordCount; // Distribute colors evenly across color wheel/palette for (int i = 0; i < SEGLEN; i++) { int patternIndex = (offset + i) % patternLength; if (GET_BIT8(morsecodePattern, patternIndex)) { uint8_t wordIdx = wordIndexArray[patternIndex]; if (SEGMENT.check1) { // make each word a separate color if (SEGMENT.custom3 < 16) // use word index to select base color, add slight offset for animation SEGMENT.setPixelColor(i, SEGMENT.color_wheel((wordIdx * colorSpacing) + (SEGENV.aux0 / 4))); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(wordIdx * colorSpacing, true, PALETTE_SOLID_WRAP, 0)); } else { if (SEGMENT.custom3 < 16) SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0 + i)); else SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } } } static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1"; /* / Perlinscape effect - a Perlin noise Landscape * Created by stepko as part of Stepko Land on soulmatelights.com * Adapted to WLED by Bob Loeffler with additional features (and help from Claude) * First slider is for speed/movement * Second slider is for zooming in/out (Perlin scaling) * Third slider is the X multiplier * Fourth slider is the Y multiplier * Fifth slider is the rotation speed (0 = do not rotate) * Check1 will randomize the horizontal and vertical directions (8 possible directions controlled by aux1) */ static void mode_2D_perlinscape(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; const uint16_t width = SEG_W; const uint16_t height = SEG_H; if (!SEGENV.allocateData(5 * sizeof(int32_t) + sizeof(uint32_t))) FX_FALLBACK_STATIC; uint32_t speedMult = map(SEGMENT.speed, 0, 255, 1, 30); uint32_t t = strip.now / 10; uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); int32_t &offX = *(reinterpret_cast(SEGENV.data) + 0); int32_t &offY = *(reinterpret_cast(SEGENV.data) + 1); int32_t &stepX = *(reinterpret_cast(SEGENV.data) + 2); int32_t &stepY = *(reinterpret_cast(SEGENV.data) + 3); int32_t &angle = *(reinterpret_cast(SEGENV.data) + 4); uint32_t &prevT = *(reinterpret_cast(reinterpret_cast(SEGENV.data) + 5)); if (SEGENV.call == 0) { SEGENV.aux0 = hw_random16(5000, 10000); SEGENV.aux1 = 0; offX = 0; offY = 0; stepX = 256; // 1.0 in Q8 stepY = 256; // 1.0 in Q8 angle = 0; prevT = t; } // change direction if (SEGMENT.check1 && (strip.now - SEGENV.step > SEGENV.aux0)) { SEGENV.aux0 = hw_random16(5000, 10000); SEGENV.aux1 = hw_random8(8); // random direction SEGENV.step = strip.now; } // direction lookup table {targetX, targetY} in Q8 format static const int16_t direction[8][2] = { { 0, -256}, // down { 256, -256}, // down-left { 256, 0}, // left { 256, 256}, // up-left { 0, 256}, // up {-256, 256}, // up-right {-256, 0}, // right {-256, -256}, // down-right }; int16_t targetX = direction[SEGENV.aux1][0]; int16_t targetY = direction[SEGENV.aux1][1]; stepX += ((targetX - stepX) * 13) >> 8; stepY += ((targetY - stepY) * 13) >> 8; // dt in raw milliseconds uint32_t udt = strip.now - prevT; int32_t dt = (udt > 500) ? 0 : (int32_t)udt; offX += (stepX * dt * speedMult) >> 14; offY += (stepY * dt * speedMult) >> 14; prevT = strip.now; int32_t tX = static_cast(static_cast(offX) << 1U); int32_t tY = static_cast(static_cast(offY) << 1U); // Rotation — cos16/sin16 return Q15 (-32768..32767 = -1.0..1.0) int32_t cosA = 1024; // Q10: 1.0 = 1024 int32_t sinA = 0; // rotate if rotation speed slider is not 0 if (SEGMENT.custom3 > 0) { int32_t dirOffset = 0; if (!SEGMENT.check1) { // rotation in one direction angle = ((angle + (int32_t)SEGMENT.custom3 * dt) & 0xFFFF); } else { // rotation on direction change float moveAngle = atan2f((float)stepY, (float)stepX); dirOffset = (int32_t)(moveAngle * 32768.0f / (float)M_PI); int8_t rotDir = (stepX >= 0) ? 1 : -1; // clockwise vs counter-clockwise angle = ((angle + rotDir * (int32_t)SEGMENT.custom3 * dt) & 0xFFFF); } cosA = cos16_t((uint16_t)(angle + dirOffset)) >> 5; sinA = sin16_t((uint16_t)(angle + dirOffset)) >> 5; } // Fixed offsets to spread the three Perlin calls into different color regions (300 just looks good) constexpr uint16_t colorOffX = 300, colorOffY = 300; // Center in Q8 (avoids 0.5 fractions) int32_t cx256 = (int32_t)width * 128; int32_t cy256 = (int32_t)height * 128; // scale: map intensity 0-255 -> 10-200, then store as Q8 (divide by 100 baked in) int32_t scale_q8 = (int32_t)map(SEGMENT.intensity, 0, 255, 10, 200) * 256 / 100; for (uint16_t x = 0; x < width; x++) { for (uint16_t y = 0; y < height; y++) { // (x - cx) and (y - cy) in Q8 int32_t dx256 = (int32_t)x * 256 - cx256; int32_t dy256 = (int32_t)y * 256 - cy256; // Rotation in Q8: cosA/sinA are Q10, dx/dy are Q8 // cosA*dx >> 10 = Q8 result; add cx256 to re-center int32_t rx256 = ((cosA * dx256 - sinA * dy256) >> 10) + cx256; int32_t ry256 = ((sinA * dx256 + cosA * dy256) >> 10) + cy256; // scaled_x = rx * Xmult * scale // rx256 is Q8, scale_q8 is Q8 => product is Q16, >> 16 gives integer int32_t scaled_x = int32_t((int64_t(rx256) * Xmult * scale_q8) >> 16); int32_t scaled_y = int32_t((int64_t(ry256) * Ymult * scale_q8) >> 16); if (SEGMENT.palette == 0) { SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x + tX, scaled_y + tY, t), perlin8(scaled_x + tX, scaled_y + tY + colorOffY), perlin8(scaled_x + tX + colorOffX, scaled_y + tY)); } else { uint8_t paletteIndex = perlin8(scaled_x + tX, scaled_y + tY, t); uint8_t brightness = perlin8(scaled_x + tX + colorOffX, scaled_y + tY + colorOffY); SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, brightness)); } } } } static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (In/Out),X multiplier,Y multiplier,Rotation speed,Random direction;;!;2;c3=0,o1=1,pal=0"; /* * Dissolve Plus * Modifications to original Dissolve effect by Bob Loeffler * slider 1 is for the delay interval between dissolving and filling * slider 2 is for the dissolving speed * slider 3 is for the filling speed * slider 4 is for the delay when only one LED is lit and when in lastOne mode (not used if Last One checkbox is not selected) * If set to max value (255), the effect will not redraw any LEDs, so this can be used with a playlist and physical button to, * for example, restart the animation by unchecking checkbox 3 in a preset and then checking it again with another preset. * This was requested in https://github.com/wled/WLED/issues/1044 * slider 5 is for the rate at which the LEDs will fade away (if set to 0, they will immediately change to the background color; this is the original Dissolve FX rate) * checkbox 1 is to select random colors * checkbox 2 is to force it to wait until all LEDs have been completely filled or dissolved * checkbox 3 is to select whether one last LED will stay lit (like a "last one standing" or "sole survivor") * aux0: 3 packed values: phase/stage of dissolve/refill process, whether done dissolving/refilling, and previous value of lastOneMode * aux1: random survivor pixel index */ #define DISSOLVE_PHASE (SEGENV.aux0 & 0xFF) #define DISSOLVE_DONE ((SEGENV.aux0 >> 8) & 0x01) #define DISSOLVE_PREV_LAST_ONE ((SEGENV.aux0 >> 9) & 0x01) #define SET_PHASE(p) (SEGENV.aux0 = (SEGENV.aux0 & 0xFF00) | (p)) #define SET_DONE(d) (SEGENV.aux0 = (SEGENV.aux0 & 0xFEFF) | ((d) << 8)) #define SET_PREV_LAST_ONE(d) (SEGENV.aux0 = (SEGENV.aux0 & 0xFDFF) | ((d) << 9)) static void mode_dissolveplus(void) { unsigned dataSize = sizeof(uint32_t) * (SEGLEN + 1); if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); uint32_t& storedBg = pixels[SEGLEN]; constexpr unsigned PHASE_FILL = 0; constexpr unsigned PHASE_DISSOLVE = 1; constexpr unsigned PHASE_FILL_SURVIVOR = 2; constexpr unsigned PHASE_DISSOLVE_SURVIVOR = 3; constexpr unsigned PHASE_PAUSE_SURVIVOR = 4; bool lastOneMode = SEGMENT.check3; if (SEGENV.call == 0) { for (unsigned i = 0; i < SEGLEN; i++) pixels[i] = SEGCOLOR(1); storedBg = SEGCOLOR(1); SET_PHASE(PHASE_DISSOLVE); SEGENV.aux1 = hw_random16(SEGLEN); SET_DONE(0); SET_PREV_LAST_ONE(lastOneMode ? 1 : 0); } else if (storedBg != SEGCOLOR(1)) { for (unsigned i = 0; i < SEGLEN; i++) { if (pixels[i] == storedBg) pixels[i] = SEGCOLOR(1); } storedBg = SEGCOLOR(1); } // Restart if lastOneMode changed to true if ((bool)DISSOLVE_PREV_LAST_ONE != lastOneMode) { if (lastOneMode) { SET_PHASE(PHASE_DISSOLVE); unsigned attempts = 0; do { SEGENV.aux1 = hw_random16(SEGLEN); attempts++; } while (pixels[SEGENV.aux1] == SEGCOLOR(1) && attempts < SEGLEN); } else { if (DISSOLVE_PHASE == PHASE_DISSOLVE_SURVIVOR) { SET_PHASE(PHASE_DISSOLVE); } else if (DISSOLVE_PHASE == PHASE_FILL_SURVIVOR) { SET_PHASE(PHASE_FILL); } } SET_DONE(0); SEGENV.step = 0; SET_PREV_LAST_ONE(lastOneMode ? 1 : 0); } // Phase 4: pause and keep only one pixel lit if (DISSOLVE_PHASE == PHASE_PAUSE_SURVIVOR) { uint16_t lastOneDelay = SEGMENT.custom2 << 1; if (lastOneDelay < 1) lastOneDelay = 1; bool freezeForever = lastOneMode && SEGMENT.custom2 == 255; if (freezeForever) { SEGENV.step++; return; } if (SEGENV.step >= lastOneDelay) { SEGENV.step = 0; SET_PHASE(PHASE_FILL_SURVIVOR); SET_DONE(0); } else { SEGENV.step++; } for (unsigned i = 0; i < SEGLEN; i++) SEGMENT.setPixelColor(i, pixels[i]); return; } bool filling = (DISSOLVE_PHASE == PHASE_FILL || DISSOLVE_PHASE == PHASE_FILL_SURVIVOR); bool forceComplete = lastOneMode && (DISSOLVE_PHASE == PHASE_FILL_SURVIVOR || DISSOLVE_PHASE == PHASE_DISSOLVE_SURVIVOR); for (unsigned j = 0; j <= SEGLEN / 15; j++) { if (hw_random8() <= (filling ? SEGMENT.custom1 : SEGMENT.intensity)) { // set the fill or dissolve speed for (size_t times = 0; times < 10; times++) { //attempt to spawn a new pixel 10 times unsigned i = hw_random16(SEGLEN); if (lastOneMode && DISSOLVE_PHASE == PHASE_DISSOLVE_SURVIVOR && i == SEGENV.aux1) continue; if (filling) { // fill with primary/palette color if (pixels[i] == storedBg) { uint32_t c; if (SEGMENT.check1) { uint8_t pId = SEGMENT.palette; c = (pId == 0) ? SEGMENT.color_wheel(hw_random8()) : SEGMENT.color_from_palette(hw_random16(SEGLEN), true, PALETTE_SOLID_WRAP, 0); if (c == SEGCOLOR(1)) c ^= 0x00000001; // flip the last bit to make sure it is slightly different than the background color pixels[i] = c; } else { c = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); if (c == SEGCOLOR(1)) c ^= 0x00000001; pixels[i] = c; } break; } } else { //dissolve to secondary/background color if (pixels[i] != storedBg) { uint8_t fadeRate = SEGENV.custom3; // (slider values are 0 -> 31) if (fadeRate > 0) { // fade progressively towards the background color by the fadeRate value uint32_t c = color_blend(pixels[i], SEGCOLOR(1), fadeRate << 2); pixels[i] = c; } else { // quickly fade to the background color if 0 is selected on custom3 slider) pixels[i] = storedBg; } break; } } } } } if (lastOneMode && (DISSOLVE_PHASE == PHASE_FILL_SURVIVOR || DISSOLVE_PHASE == PHASE_DISSOLVE_SURVIVOR)) { if (pixels[SEGENV.aux1] == SEGCOLOR(1)) { uint32_t c; if (SEGMENT.check1) { uint8_t pId = SEGMENT.palette; c = (pId == 0) ? SEGMENT.color_wheel(hw_random8()) : SEGMENT.color_from_palette(hw_random16(SEGLEN), true, PALETTE_SOLID_WRAP, 0); } else { c = SEGMENT.color_from_palette(SEGENV.aux1, true, PALETTE_SOLID_WRAP, 0); } if (c == SEGCOLOR(1)) c ^= 0x00000001; pixels[SEGENV.aux1] = c; } } unsigned incompletePixels = 0; for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[i]); // fix for #4401 if (SEGMENT.check2 || forceComplete) { if (lastOneMode && DISSOLVE_PHASE == PHASE_DISSOLVE_SURVIVOR && i == SEGENV.aux1) continue; if (filling) { if (pixels[i] == storedBg) incompletePixels++; } else { if (pixels[i] != storedBg) incompletePixels++; } } } if ((SEGMENT.check2 || forceComplete) && incompletePixels == 0 && !DISSOLVE_DONE) { SET_DONE(1); SEGENV.step = 0; } bool stepReady = (DISSOLVE_DONE || (!SEGMENT.check2 && !forceComplete)) && SEGENV.step > (255 - SEGMENT.speed) + 15U; if (stepReady) { SEGENV.step = 0; SET_DONE(0); if (!lastOneMode) { SET_PHASE(DISSOLVE_PHASE == PHASE_FILL ? PHASE_DISSOLVE : PHASE_FILL); } else { switch (DISSOLVE_PHASE) { case PHASE_FILL: SET_PHASE(PHASE_DISSOLVE); break; case PHASE_DISSOLVE: SET_PHASE(PHASE_FILL_SURVIVOR); break; case PHASE_FILL_SURVIVOR: { unsigned attempts = 0; do { SEGENV.aux1 = hw_random16(SEGLEN); attempts++; } while (pixels[SEGENV.aux1] == SEGCOLOR(1) && attempts < SEGLEN); SET_PHASE(PHASE_DISSOLVE_SURVIVOR); break; } case PHASE_DISSOLVE_SURVIVOR: SET_PHASE(PHASE_PAUSE_SURVIVOR); break; case PHASE_PAUSE_SURVIVOR: SET_PHASE(PHASE_FILL_SURVIVOR); break; } } } else { SEGENV.step++; } } #undef DISSOLVE_PHASE #undef DISSOLVE_DONE #undef DISSOLVE_PREV_LAST_ONE #undef SET_PHASE #undef SET_DONE #undef SET_PREV_LAST_ONE static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat speed,Dissolve speed,Fill speed,Last one delay,Fade rate,Random,Complete,Last one;!,!;!;;o2=1"; #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* / Pendulum effect * by Bob Loeffler and claude.ai * First slider (speed) is for the speed of the pendulum LED going back and forth. (0 = random speed) * Second slider (intensity) is for the X offset. In the middle means the center of the pendulum will be in the middle of the LED strip. * Moving the slider left will move the pendulum to the left, etc. (0 = random X offset) * Third slider (blur) is for how much of a trail the pendulum will leave. * Fourth slider (delay) is for how long the effect will wait before restarting the pendulum movement. * Fifth slider (gravity) is for how much the pendulum movement will be changed due to gravity. (0 = random gravity) * Checkbox1 will select the LED color based on it's position on the strip. * Checkbox2 will make the pendulum swing forever (a perpetual pendulum). * Checkbox3 will select the large pendulum. * aux0 stores the settings checksum to detect changes. */ // AI: below section was generated by an AI #define PENDULUM_MIN_AMPLITUDE_SUBPX ((PS_P_RADIUS_1D * 2) / 5) // Custom pendulum state, stored in the extra bytes after the particle system's own data typedef struct { int32_t currentPos; // sub-pixel position (scaled by PS_P_RADIUS_1D); matches particle.x units directly int32_t fromPos; int32_t toPos; int32_t amplitude; // sub-pixel uint16_t gravity256; // Q8 fixed point: 0-255 represents 0.0-1.0 uint32_t swingStart; uint32_t swingDuration; uint32_t pauseStart; uint16_t swing_time; uint16_t offsetX; // pixel units (kept as pixels since map() needs it that way) uint8_t phase; uint8_t goingLeft; } PendulumData; // AI: end static void mode_Pendulum(void) { ParticleSystem1D *PartSys = nullptr; PendulumData *pd = nullptr; if (SEGLEN < 4) FX_FALLBACK_STATIC; if (SEGMENT.call == 0) { if (!initParticleSystem1D(PartSys, 0, 1, sizeof(PendulumData))) FX_FALLBACK_STATIC; } else { PartSys = reinterpret_cast(SEGENV.data); } if (PartSys == nullptr) FX_FALLBACK_STATIC; PartSys->updateSystem(); pd = reinterpret_cast(PartSys->PSdataEnd); uint16_t segLen = SEGLEN; uint16_t segCenter = segLen / 2; uint16_t pause_delay = map(SEGMENT.custom2, 0, 255, 500, 20000); uint16_t sizePx = SEGMENT.check3 ? 3 : 0; // approx. particle radius in pixels; tune by eye (either 3 or 4) uint16_t cutoff = segLen / 10 + sizePx; constexpr uint8_t PHASE_STOPPED = 0U; constexpr uint8_t PHASE_SWING = 1U; constexpr uint8_t PHASE_PAUSED = 2U; PartSys->setUsedParticles(1); PartSys->setWrap(false); PartSys->setBounce(false); // AI: below section was generated by an AI uint32_t loss = 255 - SEGMENT.custom1; // amount of blurring; using loss spreads out the blur across the whole 1-255 range. loss = (loss * loss) / 255; // without this, most blur occurred near the highest setting; 0 = no blurring if (SEGMENT.custom1 > 0 && loss < 2) loss = 2; PartSys->setMotionBlur(255 - loss); // AI: end PartSys->setColorByPosition(SEGMENT.check1); uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3 + SEGMENT.check2 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); PartSys->setParticleSize(SEGMENT.check3 ? 120 : 1); // large or small particle (pendulum) if (SEGENV.call == 0 || pd->phase == PHASE_STOPPED || settingsChanged) { const uint8_t speed = SEGMENT.speed; if (speed == 0) pd->swing_time = hw_random16(1000, 2400); else pd->swing_time = map(speed, 1, 255, 2400, 400); const uint8_t intensity = SEGMENT.intensity; if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff); else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - 1 - cutoff); // AI: below section was generated by an AI // Gravity as Q8 fixed-point (0-256 = 0.0-1.0) const uint8_t custom3 = SEGMENT.custom3; uint8_t gravityPct; if (custom3 == 0) gravityPct = hw_random8(75, 95); else gravityPct = map(custom3, 1, 31, 95, 75); pd->gravity256 = ((uint16_t)gravityPct * 256) / 100; // Amplitude in pixels first, then converted to sub-pixel units uint16_t amplitude_px; if (pd->offsetX < segCenter) amplitude_px = pd->offsetX; else amplitude_px = MIN(pd->offsetX, (uint16_t)(segLen - 1 - pd->offsetX)); amplitude_px = (amplitude_px > sizePx) ? (amplitude_px - sizePx) : 0; // leave room for the particle's width at each end of the swing pd->amplitude = (int32_t)amplitude_px * PS_P_RADIUS_1D; pd->fromPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; pd->toPos = pd->fromPos - pd->amplitude; pd->swingStart = strip.now; pd->swingDuration = pd->swing_time; pd->phase = PHASE_SWING; pd->goingLeft = 1; pd->currentPos = pd->fromPos; // AI: end PartSys->particles[0].ttl = 300; PartSys->particleFlags[0].perpetual = true; PartSys->particleFlags[0].fixed = true; SEGENV.aux0 = settingssum; } if (pd->phase == PHASE_SWING) { // AI: below section was generated by an AI uint32_t elapsed = strip.now - pd->swingStart; if (elapsed > pd->swingDuration) elapsed = pd->swingDuration; // angle: 0 -> 32768 represents 0 -> 180 degrees in cos16_t's 16-bit angle space uint32_t angle = pd->swingDuration ? ((uint32_t)elapsed * 32768) / pd->swingDuration : 32768; int32_t cosT16 = cos16_t((uint16_t)angle); // inferred range: -32767..32767 int32_t frac16 = (32767 - cosT16) >> 1; // 0..32767, represents ease fraction * 32767 int32_t delta = pd->toPos - pd->fromPos; // sub-pixel pd->currentPos = pd->fromPos + (int32_t)(((int64_t)delta * frac16) / 32767); if (elapsed >= pd->swingDuration) { bool perpetual = SEGMENT.check2; if (!perpetual) { pd->amplitude = (pd->amplitude * (int32_t)pd->gravity256) >> 8; // (Q8 fixed-point) } int32_t minAmplitude = SEGMENT.check3 ? PENDULUM_MIN_AMPLITUDE_SUBPX * 3 : PENDULUM_MIN_AMPLITUDE_SUBPX; if (!perpetual && pd->amplitude < minAmplitude) { pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; pd->phase = PHASE_PAUSED; pd->pauseStart = strip.now; } else { int32_t offsetSubpx = (int32_t)pd->offsetX * PS_P_RADIUS_1D; int32_t nextTo = offsetSubpx + (pd->goingLeft ? pd->amplitude : -pd->amplitude); pd->fromPos = pd->toPos; pd->toPos = nextTo; pd->goingLeft = !(pd->goingLeft); pd->swingStart = strip.now; // duration scales with amplitude/half-segment-length, in Q8 fixed-point int32_t halfSegSubpx = ((int32_t)(segLen - 1) * PS_P_RADIUS_1D) / 2; int32_t ratio256 = halfSegSubpx ? (pd->amplitude * 256) / halfSegSubpx : 256; uint32_t dur = ((uint32_t)pd->swing_time * ratio256) >> 8; pd->swingDuration = (dur < 200) ? 200 : dur; } } } else if (pd->phase == PHASE_PAUSED) { pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; if (strip.now - pd->pauseStart >= pause_delay) { pd->phase = PHASE_STOPPED; } } // AI: end PartSys->particles[0].x = pd->currentPos; PartSys->particles[0].vx = 0; PartSys->update(); } static const char _data_FX_MODE_PENDULUM[] PROGMEM = "Pendulum@Speed (0=random),X Offset (0=random),Blur,Delay,Gravity (0=random), Position Color, Perpetual, XL;!,!;!;1;c2=32,c3=4,o1=1"; #endif // AI: below section was generated by an AI /* * Jack-o-Lantern (2D, optionally audio reactive) * A carved pumpkin with a flickering candle inside and an animated mouth. * The pumpkin is drawn procedurally (no bitmap), so it scales to any matrix size. It is recognizable from about 16x16. * All shapes are defined in normalized coordinates where the pumpkin body is the unit circle * (Q8: 256 = 1.0, v grows downwards). * Pixels are supersampled (up to 4x4 samples) so that edges and the moving mouth look smooth on small matrices. * Audio: only real AudioReactive data is used (no sound simulation). When AudioReactive is not available, or when it * stays silent for a few seconds (e.g. no microphone connected), the mouth runs its own animation. * First slider (speed) sets the speed of the mouth animation when there is no sound * Second slider (intensity) sets the strength of the candle flicker * Third slider (custom1) sets how much candle light shines through the pumpkin skin * Fourth slider (custom2) sets the audio sensitivity * Checkbox 1 selects a steady "chomp" instead of random "talking" when there is no sound * Checkbox 2 keeps the mouth animating in silence. If unchecked, the mouth stays shut until sound is heard * (only when AudioReactive is enabled) * Checkbox 3 uses the segment colors (1: pumpkin, 2: candle light, 3: stem) instead of the built-in colors */ // geometry in normalized coordinates (Q8, pumpkin body = unit circle) constexpr int32_t JACK_BODY_ASPECT = 294; // body width/height ratio (Q8, 1.15 = slightly squat) constexpr int32_t JACK_STEM_BASE = -200; // stem starts inside the top of the body constexpr int32_t JACK_STEM_HALF_W = 32; constexpr int32_t JACK_STEM_LEAN = 40; // horizontal lean of the stem per unit of height (Q8) constexpr int32_t JACK_EYE_X = 100; // horizontal distance of the eye centers from the middle constexpr int32_t JACK_EYE_TOP = -150; // eyes are triangles pointing up constexpr int32_t JACK_EYE_BOTTOM = -40; constexpr int32_t JACK_EYE_HALF_W = 58; constexpr int32_t JACK_NOSE_TOP = -25; constexpr int32_t JACK_NOSE_BOTTOM = 55; constexpr int32_t JACK_NOSE_HALF_W = 34; constexpr int32_t JACK_MOUTH_HALF_W = 180; constexpr int32_t JACK_MOUTH_TOP = 70; // upper edge of the mouth at its corners constexpr int32_t JACK_MOUTH_SMILE = 35; // how much lower the upper edge is in the middle constexpr int32_t JACK_MOUTH_SHUT = 40; // mouth height in the middle when closed constexpr int32_t JACK_MOUTH_OPEN = 80; // additional mouth height when fully open constexpr int32_t JACK_TOOTH_X = 75; // horizontal distance of the two upper teeth from the middle constexpr int32_t JACK_TOOTH_HALF_W = 22; constexpr int32_t JACK_TOOTH_LEN = 115; // tooth length relative to the local mouth height (Q8) constexpr int32_t JACK_FLAME_V = 110; // vertical position of the candle flame // shading and colors constexpr int32_t JACK_FLAME_FALLOFF = 50; // how fast candle light fades with distance from the flame constexpr int32_t JACK_RIM_DARKEN = 110; // darkening of the skin towards the outline constexpr uint8_t JACK_RIB_DEPTH = 100; // darkening of the skin between the lobes constexpr uint8_t JACK_SKIN_AMBIENT = 85; // skin brightness without any candle light shining through constexpr uint32_t JACK_SKIN_COLOR = 0xFF6000; constexpr uint32_t JACK_STEM_COLOR = 0x3A7A16; constexpr uint32_t JACK_FLAME_BRIGHT = 0xFFD848; constexpr uint32_t JACK_FLAME_DIM = 0xFF8410; // animation timing constexpr uint32_t JACK_MAX_DT = 100; // ms, limits jumps after pauses or time base changes constexpr uint32_t JACK_MOUTH_RATE_MIN = 64; // mouth animation phase increment per ms (Q8) at speed 0 constexpr uint32_t JACK_MOUTH_RATE_STEP = 3; // additional phase increment per ms for each speed step constexpr uint32_t JACK_MOUTH_ATTACK = 12; // smoothing of the mouth opening per ms (Q8) constexpr uint32_t JACK_MOUTH_RELEASE = 4; // smoothing of the mouth closing per ms (Q8) constexpr uint8_t JACK_CHOMP_REST = 48; // part of the chomp cycle where the mouth stays shut constexpr uint8_t JACK_TALK_PAUSE = 100; // talking pauses while the slow "sentence" noise is below this value constexpr uint8_t JACK_TALK_REST = 100; // syllable noise below this value keeps the mouth shut constexpr uint8_t JACK_FLARE_GAIN = 160; // candle flare from sound volume constexpr uint32_t JACK_FLARE_DECAY = 128; // candle flare fade per ms (Q8), a full flare fades out in about 0.5s // audio constexpr float JACK_SILENCE_LEVEL = 2.0f; // volume (0-255) below this value is silence constexpr uint32_t JACK_SILENCE_TIMEOUT_MS = 3000; // switch to the mouth animation after this much silence enum JackMaterial : uint8_t { JACK_BACKGROUND, JACK_SKIN, JACK_STEM, JACK_LIGHT }; struct JackState { uint32_t lastFrame; // strip.now of the previous frame uint32_t lastSound; // strip.now when sound was last heard uint32_t phase; // phase of the mouth animation (Q8) uint16_t mouthOpen; // smoothed mouth opening (0-65535) uint16_t flare; // extra candle brightness from loud sounds (Q8, keeps fractional fading for any frame time) }; /* * Returns what is visible at the normalized position (u,v): background, pumpkin skin, stem or candle light (carved face). * @param stemTop upper end of the stem (normalized v) * @param mouthHeight current height of the mouth in its middle (normalized) */ static uint8_t jackMaterialAt(int32_t u, int32_t v, int32_t stemTop, int32_t mouthHeight) { // stem, leaning slightly to the right towards its top if (v < JACK_STEM_BASE && v >= stemTop) { int32_t lean = (JACK_STEM_BASE - v) * JACK_STEM_LEAN / 256; if (abs(u - lean) <= JACK_STEM_HALF_W) return JACK_STEM; } if (u * u + v * v > 65536) return JACK_BACKGROUND; // outside of the body int32_t au = abs(u); // the face is symmetric // eyes: triangles pointing up if (v >= JACK_EYE_TOP && v <= JACK_EYE_BOTTOM && abs(au - JACK_EYE_X) * (JACK_EYE_BOTTOM - JACK_EYE_TOP) <= JACK_EYE_HALF_W * (v - JACK_EYE_TOP)) return JACK_LIGHT; // nose: small triangle pointing up if (v >= JACK_NOSE_TOP && v <= JACK_NOSE_BOTTOM && au * (JACK_NOSE_BOTTOM - JACK_NOSE_TOP) <= JACK_NOSE_HALF_W * (v - JACK_NOSE_TOP)) return JACK_LIGHT; // mouth: smile-shaped slit that tapers towards the corners, its lower edge drops when the mouth opens if (au < JACK_MOUTH_HALF_W) { int32_t profile = 256 - au * au * 256 / (JACK_MOUTH_HALF_W * JACK_MOUTH_HALF_W); // 256 in the middle, 0 at the corners int32_t top = JACK_MOUTH_TOP + JACK_MOUTH_SMILE * profile / 256; int32_t height = mouthHeight * profile / 256; int32_t bottom = top + height; if (v >= top && v <= bottom) { int32_t tooth = height * JACK_TOOTH_LEN / 256; if (abs(au - JACK_TOOTH_X) < JACK_TOOTH_HALF_W && v < top + tooth) return JACK_SKIN; // two upper teeth if (au < JACK_TOOTH_HALF_W && v > bottom - tooth) return JACK_SKIN; // one lower tooth return JACK_LIGHT; } } return JACK_SKIN; } static void mode_2D_jackolantern(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up if (!SEGENV.allocateData(sizeof(JackState))) FX_FALLBACK_STATIC; // allocation failed JackState* state = reinterpret_cast(SEGENV.data); int cols = SEG_W; int rows = SEG_H; uint32_t now = strip.now; if (SEGENV.call == 0) { state->lastFrame = now; state->lastSound = now - JACK_SILENCE_TIMEOUT_MS; // start in silence until sound is heard state->phase = 0; state->mouthOpen = 0; state->flare = 0; } uint32_t dt = min(now - state->lastFrame, JACK_MAX_DT); state->lastFrame = now; // audio: use real AudioReactive data only, do not use simulated sound um_data_t *um_data; bool haveAudio = UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE); uint8_t volume = 0; bool peak = false; if (haveAudio) { float volumeSmth = *(float*)um_data->u_data[0]; peak = *(uint8_t*)um_data->u_data[3]; if (volumeSmth > JACK_SILENCE_LEVEL) state->lastSound = now; volume = constrain(int(volumeSmth * float(SEGMENT.custom2 + 16) / 128.0f), 0, 255); // sensitivity: x0.125 to x2.1 } bool hearing = haveAudio && (now - state->lastSound < JACK_SILENCE_TIMEOUT_MS); // mouth: follow the sound, otherwise talk or chomp on its own uint8_t target = 0; if (hearing) { target = volume; } else if (!haveAudio || SEGMENT.check2) { state->phase += dt * (JACK_MOUTH_RATE_MIN + SEGMENT.speed * JACK_MOUTH_RATE_STEP); if (SEGMENT.check1) { target = qsub8(sin8_t(uint8_t(state->phase >> 10)), JACK_CHOMP_REST) * 255 / (255 - JACK_CHOMP_REST); } else { // syllables from fast noise, pauses between "sentences" from slow noise // (perlin noise is periodic in 16 bit, so the casts wrap seamlessly) uint8_t sentence = perlin8(uint16_t(state->phase >> 11)); uint8_t syllable = perlin8(uint16_t(state->phase >> 8)); if (sentence >= JACK_TALK_PAUSE) target = min(qsub8(syllable, JACK_TALK_REST) * 2, 255); } } int32_t diff = (int32_t(target) << 8) - state->mouthOpen; int32_t alpha = min(dt * (diff > 0 ? JACK_MOUTH_ATTACK : JACK_MOUTH_RELEASE), uint32_t(256)); state->mouthOpen += diff * alpha / 256; int32_t mouthHeight = JACK_MOUTH_SHUT + (state->mouthOpen >> 8) * JACK_MOUTH_OPEN / 255; // candle: two octaves of noise, intensity sets the flicker depth, loud sounds make it flare uint8_t fastNoise = perlin8(uint16_t(now << 1)); uint8_t slowNoise = perlin8(uint16_t((now >> 1) + 0x8000)); uint8_t level = 255 - scale8(255 - ((fastNoise * 3 + slowNoise) >> 2), SEGMENT.intensity); uint32_t flareFade = dt * JACK_FLARE_DECAY; state->flare = (state->flare > flareFade) ? state->flare - flareFade : 0; if (hearing) { state->flare = max(state->flare, uint16_t(scale8(volume, JACK_FLARE_GAIN) << 8)); if (peak) state->flare = 255 << 8; } level += scale8(255 - level, state->flare >> 8); int32_t flameU = (int32_t(slowNoise) - 128) * SEGMENT.intensity / 512; // the flame sways sideways uint8_t skinBri = JACK_SKIN_AMBIENT + scale8(255 - JACK_SKIN_AMBIENT, scale8(level, SEGMENT.custom1)); bool customColors = SEGMENT.check3; uint32_t skinColor = customColors ? SEGCOLOR(0) : JACK_SKIN_COLOR; uint32_t lightColor = customColors ? SEGCOLOR(1) : JACK_FLAME_BRIGHT; uint32_t dimColor = customColors ? SEGCOLOR(1) : JACK_FLAME_DIM; uint32_t stemColor = customColors ? SEGCOLOR(2) : JACK_STEM_COLOR; // size and position of the pumpkin (Q8 pixels): leave 1/8 of the height for the stem, keep the body aspect ratio int32_t ry = rows * 112; int32_t rx = cols * 128; if (rx * 256 > ry * JACK_BODY_ASPECT) { rx = ry * JACK_BODY_ASPECT / 256; // wide matrix: limit the width } else { ry = rx * 256 / JACK_BODY_ASPECT; // square or tall matrix: limit the height } int32_t stemH = ry * 2 / 7; int32_t top = (rows * 256 - 2 * ry - stemH) / 2; // center vertically int32_t cx = cols * 128; int32_t cy = top + stemH + ry; int32_t invRx = (1 << 24) / rx; // pixel (Q8) to normalized (Q8) conversion factors (Q16) int32_t invRy = (1 << 24) / ry; int32_t stemTop = -256 - stemH * 256 / ry; unsigned lobes = (rx >= 6 * 256) ? 5 : 3; // fewer lobes on small matrices to keep the ribs visible // supersampling: keep the number of samples per frame roughly constant unsigned pixelCount = cols * rows; unsigned ss = (pixelCount <= 400) ? 4 : (pixelCount <= 1600) ? 2 : 1; // samples per axis unsigned samples = ss * ss; int32_t subU[4], subV[4]; for (unsigned i = 0; i < ss; i++) { int32_t offset = int32_t((2 * i + 1) * 128 / ss) - 128; // sample position relative to the pixel center (Q8 pixels) subU[i] = offset * invRx / 65536; subV[i] = offset * invRy / 65536; } // only the bounding box of the pumpkin needs to be rendered int yStart = top / 256; int yEnd = min(rows - 1, int((top + 2 * ry + stemH) / 256)); int xStart = (cx - rx) / 256; int xEnd = min(cols - 1, int((cx + rx) / 256)); SEGMENT.fill(BLACK); for (int y = yStart; y <= yEnd; y++) { int32_t vc = (y * 256 + 128 - cy) * invRy / 65536; // half width of the body in this row, used to bend the ribs around the body int32_t rowHalf = max(int32_t(sqrt32_bw(max(int32_t(0), 65536 - vc * vc))), int32_t(16)); int32_t rowInv = 65536 / rowHalf; for (int x = xStart; x <= xEnd; x++) { int32_t uc = (x * 256 + 128 - cx) * invRx / 65536; unsigned nSkin = 0, nStem = 0, nLight = 0; for (unsigned j = 0; j < ss; j++) { for (unsigned i = 0; i < ss; i++) { switch (jackMaterialAt(uc + subU[i], vc + subV[j], stemTop, mouthHeight)) { case JACK_SKIN: nSkin++; break; case JACK_STEM: nStem++; break; case JACK_LIGHT: nLight++; break; default: break; } } } if (nSkin + nStem + nLight == 0) continue; // background // average color of the samples that are not carved out (skin, stem and background) uint32_t r = 0, g = 0, b = 0, w = 0; const auto addColor = [&](uint32_t c, unsigned n) { r += R(c) * n; g += G(c) * n; b += B(c) * n; w += W(c) * n; }; if (nSkin) { int32_t d2 = min(uc * uc + vc * vc, int32_t(65536)); uint8_t rim = 255 - d2 * JACK_RIM_DARKEN / 65536; int32_t lobeU = constrain(uc * rowInv / 256, int32_t(-256), int32_t(256)); // position across the curved surface unsigned lobePos = ((lobeU + 256) * lobes) & 511; // position within a lobe uint8_t bump = (sin8_t(lobePos >> 2) - 128) * 2; // half sine wave: 0 at the ribs, 254 in the middle of a lobe uint8_t ribs = 255 - scale8(JACK_RIB_DEPTH, 255 - bump); addColor(color_fade(skinColor, scale8(scale8(rim, ribs), skinBri)), nSkin); } if (nStem) addColor(stemColor, nStem); unsigned nOther = samples - nLight; uint32_t color = nOther ? RGBW32(r / nOther, g / nOther, b / nOther, w / nOther) : BLACK; if (nLight) { int32_t du = uc - flameU; int32_t dv = vc - JACK_FLAME_V; uint8_t falloff = min((du * du + dv * dv) * JACK_FLAME_FALLOFF / 65536, int32_t(255)); uint8_t bri = scale8(level, 255 - falloff); uint32_t light = color_fade(color_blend(dimColor, lightColor, bri), bri); // the carved face glows: a pixel that is half covered is already fully lit, keeps small faces readable color = color_blend(color, light, min(nLight * 510 / samples, 255u)); } SEGMENT.setPixelColorXY(x, y, color); } } } static const char _data_FX_MODE_2D_JACKOLANTERN[] PROGMEM = "Jack-o-Lantern@Mouth speed,Flicker,Glow,Sensitivity,,Chomp,Animate in silence,Custom colors;Pumpkin,Light,Stem;;2v;sx=96,ix=128,c1=96,c2=128,o2=1"; // AI: end ///////////////////// // UserMod Class // ///////////////////// class UserFxUsermod : public Usermod { private: public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); strip.addEffect(255, &mode_spinning_wheel, _data_FX_MODE_SPINNINGWHEEL); strip.addEffect(255, &mode_2D_lavalamp, _data_FX_MODE_2D_LAVALAMP); strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA); strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS); strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); #ifndef WLED_DISABLE_PARTICLESYSTEM1D strip.addEffect(255, &mode_Pendulum, _data_FX_MODE_PENDULUM); #endif strip.addEffect(255, &mode_2D_jackolantern, _data_FX_MODE_2D_JACKOLANTERN); //////////////////////////////////////// // add your effect function(s) here // //////////////////////////////////////// // use id=255 for all custom user FX (the final id is assigned when adding the effect) // strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT); // strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2); // strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3); } /////////////////////////////////////////////////////////////////////////////////////////////// // If you want configuration options in the usermod settings page, implement these methods // /////////////////////////////////////////////////////////////////////////////////////////////// // void addToConfig(JsonObject& root) override // { // JsonObject top = root.createNestedObject(FPSTR("User FX")); // top["myConfigValue"] = myConfigValue; // } // bool readFromConfig(JsonObject& root) override // { // JsonObject top = root[FPSTR("User FX")]; // bool configComplete = !top.isNull(); // configComplete &= getJsonValue(top["myConfigValue"], myConfigValue); // return configComplete; // } void loop() override {} // nothing to do in the loop uint16_t getId() override { return USERMOD_ID_USER_FX; } }; static UserFxUsermod user_fx; REGISTER_USERMOD(user_fx);