diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index cf7a651db6..8971f32fde 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1258,6 +1258,125 @@ static void mode_morsecode(void) { 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) +* Checkbox will randomize the horizontal and vertical directions +*/ +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 = 0b00; + offX = 0; + offY = 0; + stepX = 256; // 1.0 in Q8 + stepY = 256; // 1.0 in Q8 + angle = 0; + prevT = t; + } + + if (SEGMENT.check1 && (strip.now - SEGENV.step > SEGENV.aux0)) { + SEGENV.aux0 = hw_random16(5000, 10000); + SEGENV.aux1 = hw_random8(4); + SEGENV.step = strip.now; + } + + bool flipX = SEGMENT.check1 ? (SEGENV.aux1 & 0x01) : false; + bool flipY = SEGMENT.check1 ? (SEGENV.aux1 & 0x02) : false; + + // targetX/Y: +256 or -256 in Q8 + int32_t targetX = flipX ? -256 : 256; + int32_t targetY = flipY ? -256 : 256; + + stepX += ((targetX - stepX) * 13) >> 8; + stepY += ((targetY - stepY) * 13) >> 8; + + // dt in raw milliseconds; offX/offY accumulate scaled by speedMult + uint32_t udt = strip.now - prevT; + int32_t dt = (udt > 500) ? 0 : (int32_t)udt; + offX += (stepX * dt * speedMult) >> 13; + offY += (stepY * dt * speedMult) >> 13; + prevT = strip.now; + + // Integer pixel offsets — gradual drift motion (Q8 >> 8 = integer) + int32_t tX = offX << 1; + int32_t tY = offY << 1; + + // Fixed offsets to spread the three Perlin calls into different color regions (300 just looks good) + constexpr uint16_t colorOffX = 300; + constexpr uint16_t colorOffY = 300; + + // Rotation — cos16/sin16 return Q15 (-32768..32767 = -1.0..1.0) + int32_t cosA = 1024; // Q10: 1.0 = 1024 + int32_t sinA = 0; + + // Center in Q8 (avoids 0.5 fractions) + int32_t cx256 = (int32_t)width * 128; + int32_t cy256 = (int32_t)height * 128; + + // rotate if rotation speed is not 0 + if (SEGMENT.custom3 > 0) { + angle = ((angle + (int32_t)SEGMENT.custom3 * dt * 2) & 0xFFFF); + cosA = cos16_t((uint16_t)angle) >> 5; + sinA = sin16_t((uint16_t)angle) >> 5; + } + + // scale: map intensity 0-255 -> 10-200, then store as Q8 (divide by 100 baked in) + // scale_q8 = map(...) * 256 / 100 + 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;sx=64,c3=0,o1=1"; + + ///////////////////// // UserMod Class // ///////////////////// @@ -1272,6 +1391,7 @@ class UserFxUsermod : public Usermod { 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); //////////////////////////////////////// // add your effect function(s) here //