Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t*>(SEGENV.data) + 0);
int32_t &offY = *(reinterpret_cast<int32_t*>(SEGENV.data) + 1);
int32_t &stepX = *(reinterpret_cast<int32_t*>(SEGENV.data) + 2);
int32_t &stepY = *(reinterpret_cast<int32_t*>(SEGENV.data) + 3);
int32_t &angle = *(reinterpret_cast<int32_t*>(SEGENV.data) + 4);
uint32_t &prevT = *(reinterpret_cast<uint32_t*>(reinterpret_cast<int32_t*>(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 //
/////////////////////
Expand All @@ -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 //
Expand Down
Loading