mirror of
https://github.com/wled/WLED.git
synced 2026-03-13 08:29:49 +08:00
these were leftover after refactoring paletteBlend into a global variable. see #5295 for details
1313 lines
51 KiB
C++
1313 lines
51 KiB
C++
#include "wled.h"
|
||
|
||
// for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata
|
||
|
||
// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
|
||
#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3)
|
||
|
||
#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16))
|
||
|
||
// static effect, used if an effect fails to initialize
|
||
static void mode_static(void) {
|
||
SEGMENT.fill(SEGCOLOR(0));
|
||
}
|
||
|
||
#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<uint32_t*>(SEGENV.data);
|
||
// state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction)
|
||
// state[1] = velocity (fixed point: pixels per frame * 65536)
|
||
// state[2] = phase (0=fast spin, 1=slowing, 2=wobble, 3=stopped)
|
||
// state[3] = stop time (when phase 3 was entered)
|
||
// state[4] = wobble step (0=at stop pos, 1=moved back, 2=returned to stop)
|
||
// state[5] = slowdown start time (when to transition from phase 0 to phase 1)
|
||
// state[6] = wobble timing (for 200ms / 400ms / 300ms delays)
|
||
// state[7] = store the stop position per strip
|
||
|
||
// state[] index values for easier readability
|
||
constexpr unsigned CUR_POS_IDX = 0; // state[0]
|
||
constexpr unsigned VELOCITY_IDX = 1;
|
||
constexpr unsigned PHASE_IDX = 2;
|
||
constexpr unsigned STOP_TIME_IDX = 3;
|
||
constexpr unsigned WOBBLE_STEP_IDX = 4;
|
||
constexpr unsigned SLOWDOWN_TIME_IDX = 5;
|
||
constexpr unsigned WOBBLE_TIME_IDX = 6;
|
||
constexpr unsigned STOP_POS_IDX = 7;
|
||
|
||
SEGMENT.fill(SEGCOLOR(1));
|
||
|
||
// Handle random seeding globally (outside the virtual strip)
|
||
if (SEGENV.call == 0) {
|
||
random16_set_seed(hw_random16());
|
||
SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling
|
||
}
|
||
|
||
// Check if settings changed (do this once, not per virtual strip)
|
||
uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check1 + 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, 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] = 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, (strips + spinnerSize - 1) / spinnerSize);
|
||
hue = (uint32_t)(255) * (stripNr / spinnerSize) / numSpinners;
|
||
} else {
|
||
// Color changes with position
|
||
hue = (SEGENV.aux1 * pos) >> 8;
|
||
}
|
||
|
||
uint32_t color = ColorFromPaletteWLED(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<LavaParticle*>(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 = sqrt(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 = ColorFromPaletteWLED(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<float*>(SEGENV.data);
|
||
float* ff_y = &particleData[MAGMA_MAX_PARTICLES * 4];
|
||
float* ff_z = &particleData[MAGMA_MAX_PARTICLES * 4 + 1];
|
||
uint32_t* settingsSumPtr = reinterpret_cast<uint32_t*>(&particleData[MAGMA_MAX_PARTICLES * 4 + 2]);
|
||
uint8_t* shiftHue = reinterpret_cast<uint8_t*>(reinterpret_cast<uint8_t*>(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<Ant*>(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<unsigned long>(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<uint8_t*>(SEGENV.data);
|
||
uint8_t* wordIndexArray = reinterpret_cast<uint8_t*>(SEGENV.data + MORSECODE_PATTERN_BYTES);
|
||
uint8_t* wordCountPtr = reinterpret_cast<uint8_t*>(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";
|
||
|
||
|
||
/////////////////////
|
||
// 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);
|
||
|
||
////////////////////////////////////////
|
||
// 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);
|