/* Festival Stick - Corkscrew LED Mapping Demo This example demonstrates proper corkscrew LED mapping for a festival stick (19+ turns, 288 LEDs) using the new Corkscrew ScreenMap functionality. Key Features: - Uses Corkscrew.toScreenMap() for accurate web interface visualization - Draws patterns into a rectangular grid (frameBuffer) - Maps the rectangular grid to the corkscrew LED positions using readFrom() - Supports both noise patterns and manual LED positioning - Proper color boost and brightness controls Workflow: 1. Draw patterns into frameBuffer (rectangular grid for easy 2D drawing) 2. Use corkscrew.readFrom(frameBuffer) to map grid to corkscrew LED positions 3. Display the corkscrew buffer directly via FastLED 4. Web interface shows actual corkscrew spiral shape via ScreenMap */ #include "FastLED.h" #include "fl/compiler_control.h" #include "fl/assert.h" #include "fl/corkscrew.h" #include "fl/grid.h" #include "fl/leds.h" #include "fl/screenmap.h" #include "fl/sstream.h" #include "fl/warn.h" #include "noise.h" #include "fl/array.h" #include "fx/2d/wave.h" #include "fx/2d/blend.h" #include "fx/fx_engine.h" #include "fx/2d/animartrix.hpp" // #include "vec3.h" using namespace fl; #ifndef PIN_DATA #define PIN_DATA 1 // Universally available pin #endif #ifndef PIN_CLOCK #define PIN_CLOCK 2 // Universally available pin #endif #ifdef TEST #define NUM_LEDS 4 #define CORKSCREW_TURNS 2 // Default to 19 turns #else #define NUM_LEDS 288 #define CORKSCREW_TURNS 19.25 // Default to 19 turns #endif // #define CM_BETWEEN_LEDS 1.0 // 1cm between LEDs // #define CM_LED_DIAMETER 0.5 // 0.5cm LED diameter UITitle festivalStickTitle("Festival Stick - Advanced Version"); UIDescription festivalStickDescription( "# Festival Stick Demo\n\n" "This example demonstrates **proper corkscrew LED mapping** for a festival stick using FastLED's advanced mapping capabilities.\n\n" "## Key Features\n" "- **19+ turns** with 288 LEDs total\n" "- Uses `Corkscrew.toScreenMap()` for accurate web interface visualization\n" "- Multiple render modes: **Noise**, **Position**, **Fire**, **Wave**, and **Animartrix** effects\n" "- Real-time cylindrical surface mapping\n" "- **Wave mode**: Cylindrical 2D wave simulation with ripple effects and configurable blur\n" "- **Animartrix mode**: Advanced 2D animation effects with polar coordinate patterns\n\n" "## How It Works\n" "1. Draws patterns into a rectangular grid (`frameBuffer`)\n" "2. Maps the grid to corkscrew LED positions using `readFrom()`\n" "3. Web interface shows the actual spiral shape via ScreenMap\n\n" "*Select different render modes and adjust parameters to see various effects!*"); // UIHelp festivalStickHelp("Festival Stick - Advanced Guide"); // UIHelp corkscrewMappingHelp("Understanding Corkscrew Mapping"); // UIHelp uiControlsHelp("UI Controls Guide"); UISlider speed("Speed", 0.1f, 0.01f, 1.0f, 0.01f); UISlider positionCoarse("Position Coarse (10x)", 0.0f, 0.0f, 1.0f, 0.01f); UISlider positionFine("Position Fine (1x)", 0.0f, 0.0f, 0.1f, 0.001f); UISlider positionExtraFine("Position Extra Fine (0.1x)", 0.0f, 0.0f, 0.01f, 0.0001f); UISlider brightness("Brightness", 255, 0, 255, 1); UICheckbox autoAdvance("Auto Advance", true); UICheckbox allWhite("All White", false); UICheckbox splatRendering("Splat Rendering", true); // Noise controls (grouped under noiseGroup) UISlider noiseScale("Noise Scale", 100, 10, 200, 5); UISlider noiseSpeed("Noise Speed", 4, 1, 100, 1); // UIDropdown examples - noise-related color palette string paletteOptions[] = {"Party", "Heat", "Ocean", "Forest", "Rainbow"}; string renderModeOptions[] = { "Wave", "Animartrix", "Noise", "Position", "Fire" }; UIDropdown paletteDropdown("Color Palette", paletteOptions); UIDropdown renderModeDropdown("Render Mode", renderModeOptions); // fl::array> easeInfo = { // pair(EASE_IN_QUAD, "EASE_IN_QUAD"), // pair(EASE_OUT_QUAD, "EASE_OUT_QUAD"), // pair(EASE_IN_OUT_QUAD, "EASE_IN_OUT_QUAD"), // pair(EASE_IN_CUBIC, "EASE_IN_CUBIC"), // pair(EASE_OUT_CUBIC, "EASE_OUT_CUBIC"), // pair(EASE_IN_OUT_CUBIC, "EASE_IN_OUT_CUBIC"), // pair(EASE_IN_SINE, "EASE_IN_SINE"), // pair(EASE_OUT_SINE, "EASE_OUT_SINE"), // pair(EASE_IN_OUT_SINE, "EASE_IN_OUT_SINE") // }; fl::vector easeInfo = { "EASE_NONE", "EASE_IN_QUAD", "EASE_OUT_QUAD", "EASE_IN_OUT_QUAD", "EASE_IN_CUBIC", "EASE_OUT_CUBIC", "EASE_IN_OUT_CUBIC", "EASE_IN_SINE", "EASE_OUT_SINE", "EASE_IN_OUT_SINE" }; EaseType getEaseType(fl::string value) { if (value == "EASE_NONE") { return EASE_NONE; } else if (value == "EASE_IN_QUAD") { return EASE_IN_QUAD; } else if (value == "EASE_OUT_QUAD") { return EASE_OUT_QUAD; } else if (value == "EASE_IN_OUT_QUAD") { return EASE_IN_OUT_QUAD; } else if (value == "EASE_IN_CUBIC") { return EASE_IN_CUBIC; } else if (value == "EASE_OUT_CUBIC") { return EASE_OUT_CUBIC; } else if (value == "EASE_IN_OUT_CUBIC") { return EASE_IN_OUT_CUBIC; } else if (value == "EASE_IN_SINE") { return EASE_IN_SINE; } else if (value == "EASE_OUT_SINE") { return EASE_OUT_SINE; } else if (value == "EASE_IN_OUT_SINE") { return EASE_IN_OUT_SINE; } else { return EASE_NONE; } } // Color boost controls UIDropdown saturationFunction("Saturation Function", easeInfo); UIDropdown luminanceFunction("Luminance Function", easeInfo); // Fire-related UI controls (added for cylindrical fire effect) UISlider fireScaleXY("Fire Scale", 8, 1, 100, 1); UISlider fireSpeedY("Fire SpeedY", 1.3, 1, 6, .1); UISlider fireScaleX("Fire ScaleX", .3, 0.1, 3, .01); UISlider fireInvSpeedZ("Fire Inverse SpeedZ", 20, 1, 100, 1); UINumberField firePalette("Fire Palette", 0, 0, 2); // Wave-related UI controls (cylindrical wave effects) UISlider waveSpeed("Wave Speed", 0.03f, 0.0f, 1.0f, 0.01f); UISlider waveDampening("Wave Dampening", 9.1f, 0.0f, 20.0f, 0.1f); UICheckbox waveHalfDuplex("Wave Half Duplex", true); UICheckbox waveAutoTrigger("Wave Auto Trigger", true); UISlider waveTriggerSpeed("Wave Trigger Speed", 0.5f, 0.0f, 1.0f, 0.01f); UIButton waveTriggerButton("Trigger Wave"); UINumberField wavePalette("Wave Palette", 0, 0, 2); // Wave blur controls (added for smoother wave effects) UISlider waveBlurAmount("Wave Blur Amount", 50, 0, 172, 1); UISlider waveBlurPasses("Wave Blur Passes", 1, 1, 10, 1); // Fire color palettes (from FireCylinder) DEFINE_GRADIENT_PALETTE(firepal){ 0, 0, 0, 0, 32, 255, 0, 0, 190, 255, 255, 0, 255, 255, 255, 255 }; DEFINE_GRADIENT_PALETTE(electricGreenFirePal){ 0, 0, 0, 0, 32, 0, 70, 0, 190, 57, 255, 20, 255, 255, 255, 255 }; DEFINE_GRADIENT_PALETTE(electricBlueFirePal){ 0, 0, 0, 0, 32, 0, 0, 70, 128, 20, 57, 255, 255, 255, 255, 255 }; // Wave color palettes (for cylindrical wave effects) DEFINE_GRADIENT_PALETTE(waveBluepal){ 0, 0, 0, 0, // Black (no wave) 32, 0, 0, 70, // Dark blue (low wave) 128, 20, 57, 255, // Electric blue (medium wave) 255, 255, 255, 255 // White (high wave) }; DEFINE_GRADIENT_PALETTE(waveGreenpal){ 0, 0, 0, 0, // Black (no wave) 8, 128, 64, 64, // Green with red tint (very low wave) 16, 255, 222, 222, // Pinkish red (low wave) 64, 255, 255, 255, // White (medium wave) 255, 255, 255, 255 // White (high wave) }; DEFINE_GRADIENT_PALETTE(waveRainbowpal){ 0, 255, 0, 0, // Red (no wave) 64, 255, 127, 0, // Orange (low wave) 128, 255, 255, 0, // Yellow (medium wave) 192, 0, 255, 0, // Green (high wave) 255, 0, 0, 255 // Blue (maximum wave) }; // Create UIGroup for noise controls using variadic constructor // This automatically assigns all specified controls to the "Noise Controls" group UIGroup noiseGroup("Noise Controls", noiseScale, noiseSpeed, paletteDropdown); UIGroup fireGroup("Fire Controls", fireScaleXY, fireSpeedY, fireScaleX, fireInvSpeedZ, firePalette); UIGroup waveGroup("Wave Controls", waveSpeed, waveDampening, waveHalfDuplex, waveAutoTrigger, waveTriggerSpeed, waveTriggerButton, wavePalette, waveBlurAmount, waveBlurPasses); UIGroup renderGroup("Render Options", renderModeDropdown, splatRendering, allWhite, brightness); UIGroup colorBoostGroup("Color Boost", saturationFunction, luminanceFunction); UIGroup pointGraphicsGroup("Point Graphics Mode", speed, positionCoarse, positionFine, positionExtraFine, autoAdvance); // Animartrix-related UI controls UINumberField animartrixIndex("Animartrix Animation", 5, 0, NUM_ANIMATIONS - 1); UINumberField animartrixColorOrder("Animartrix Color Order", 0, 0, 5); UISlider animartrixTimeSpeed("Animartrix Time Speed", 1, -10, 10, .1); UIGroup animartrixGroup("Animartrix Controls", animartrixIndex, animartrixTimeSpeed, animartrixColorOrder); // Color palette for noise CRGBPalette16 noisePalette = PartyColors_p; uint8_t colorLoop = 1; // Option 1: Runtime Corkscrew (flexible, configurable at runtime) Corkscrew corkscrew(CORKSCREW_TURNS, NUM_LEDS); // Simple position tracking - one variable for both modes static float currentPosition = 0.0f; static uint32_t lastUpdateTime = 0; // Wave effect globals static uint32_t nextWaveTrigger = 0; // Option 2: Constexpr dimensions for compile-time array sizing constexpr uint16_t CORKSCREW_WIDTH = calculateCorkscrewWidth(CORKSCREW_TURNS, NUM_LEDS); constexpr uint16_t CORKSCREW_HEIGHT = calculateCorkscrewHeight(CORKSCREW_TURNS, NUM_LEDS); // Now you can use these for array initialization: // CRGB frameBuffer[CORKSCREW_WIDTH * CORKSCREW_HEIGHT]; // Compile-time sized // array // Create a corkscrew with: // - 30cm total length (300mm) // - 5cm width (50mm) // - 2mm LED inner diameter // - 24 LEDs per turn // ScreenMap screenMap = makeCorkScrew(NUM_LEDS, // 300.0f, 50.0f, 2.0f, 24.0f); // vector mapCorkScrew = makeCorkScrew(args); ScreenMap screenMap; fl::shared_ptr> frameBufferPtr; // Wave effect objects - declared here but initialized in setup() WaveFxPtr waveFx; Blend2dPtr waveBlend; // Animartrix effect objects - declared here but initialized in setup() fl::unique_ptr animartrix; fl::unique_ptr fxEngine; WaveCrgbGradientMapPtr crgMap = fl::make_shared(); void setup() { // Use constexpr dimensions (computed at compile time) constexpr int width = CORKSCREW_WIDTH; // = 16 constexpr int height = CORKSCREW_HEIGHT; // = 18 // Noise controls are now automatically grouped by the UIGroup constructor // The noiseGroup variadic constructor automatically called setGroup() on all controls // Or use runtime corkscrew for dynamic sizing // int width = corkscrew.cylinder_width(); // int height = corkscrew.cylinder_height(); XYMap xyMap = XYMap::constructRectangularGrid(width, height, 0); // Use the corkscrew's internal buffer for the LED strip CLEDController *controller = &FastLED.addLeds(corkscrew.rawData(), NUM_LEDS); // CLEDController *controller = // &FastLED.addLeds(stripLeds, NUM_LEDS); // NEW: Create ScreenMap directly from Corkscrew using toScreenMap() // This maps each LED index to its exact position on the corkscrew spiral // instead of using a rectangular grid mapping ScreenMap corkscrewScreenMap = corkscrew.toScreenMap(0.2f); // OLD WAY (rectangular grid - not accurate for corkscrew visualization): // ScreenMap screenMap = xyMap.toScreenMap(); // screenMap.setDiameter(.2f); // Set the corkscrew screen map for the controller // This allows the web interface to display the actual corkscrew spiral shape controller->setScreenMap(corkscrewScreenMap); // Initialize wave effects for cylindrical surface XYMap xyRect(width, height, false); // Rectangular grid for wave simulation WaveFx::Args waveArgs; waveArgs.factor = SuperSample::SUPER_SAMPLE_2X; // 2x supersampling for smoother waves waveArgs.half_duplex = true; // Only positive waves waveArgs.auto_updates = true; // Auto-update simulation waveArgs.speed = 0.16f; // Wave propagation speed waveArgs.dampening = 6.0f; // Wave energy loss waveArgs.x_cyclical = true; // Enable cylindrical wrapping! waveArgs.crgbMap = fl::make_shared(waveBluepal); // Default color palette // Create wave effect with cylindrical mapping waveFx = fl::make_shared(xyRect, waveArgs); // Create blender for wave effects (allows multiple wave layers in future) waveBlend = fl::make_shared(xyRect); waveBlend->add(waveFx); // Initialize Animartrix effect XYMap animartrixXyMap = XYMap::constructRectangularGrid(width, height, 0); animartrix.reset(new Animartrix(animartrixXyMap, POLAR_WAVES)); fxEngine.reset(new FxEngine(width * height)); fxEngine->addFx(*animartrix); // Demonstrate UIGroup functionality for noise controls FL_WARN("Noise UI Group initialized: " << noiseGroup.name()); FL_WARN(" This group contains noise pattern controls:"); FL_WARN(" - Use Noise Pattern toggle"); FL_WARN(" - Noise Scale and Speed sliders"); FL_WARN(" - Color Palette selection for noise"); FL_WARN(" UIGroup automatically applied group membership via variadic constructor"); // Set initial dropdown selections paletteDropdown.setSelectedIndex(0); // Party renderModeDropdown.setSelectedIndex(0); // Fire (new default) // Add onChange callbacks for dropdowns paletteDropdown.onChanged([](UIDropdown &dropdown) { string selectedPalette = dropdown.value(); FL_WARN("Noise palette changed to: " << selectedPalette); if (selectedPalette == "Party") { noisePalette = PartyColors_p; } else if (selectedPalette == "Heat") { noisePalette = HeatColors_p; } else if (selectedPalette == "Ocean") { noisePalette = OceanColors_p; } else if (selectedPalette == "Forest") { noisePalette = ForestColors_p; } else if (selectedPalette == "Rainbow") { noisePalette = RainbowColors_p; } }); renderModeDropdown.onChanged([](UIDropdown &dropdown) { string mode = dropdown.value(); // Simple example of using getOption() for(size_t i = 0; i < dropdown.getOptionCount(); i++) { if(dropdown.getOption(i) == mode) { FL_WARN("Render mode changed to: " << mode); } } }); // Add onChange callback for animartrix color order animartrixColorOrder.onChanged([](int value) { EOrder order = RGB; switch(value) { case 0: order = RGB; break; case 1: order = RBG; break; case 2: order = GRB; break; case 3: order = GBR; break; case 4: order = BRG; break; case 5: order = BGR; break; } if (animartrix.get()) { animartrix->setColorOrder(order); } }); waveFx->setCrgbMap(crgMap); frameBufferPtr = corkscrew.getOrCreateInputSurface(); } FL_OPTIMIZATION_LEVEL_O0_BEGIN // Works around a compile bug in clang 19 float get_position(uint32_t now) { if (autoAdvance.value()) { // Check if auto-advance was just enabled // Auto-advance mode: increment smoothly from current position float elapsedSeconds = float(now - lastUpdateTime) / 1000.0f; float increment = elapsedSeconds * speed.value() * 0.3f; // Make it 1/20th the original speed currentPosition = fmodf(currentPosition + increment, 1.0f); lastUpdateTime = now; return currentPosition; } else { // Manual mode: use the dual slider control float combinedPosition = positionCoarse.value() + positionFine.value() + positionExtraFine.value(); // Clamp to ensure we don't exceed 1.0 if (combinedPosition > 1.0f) combinedPosition = 1.0f; return combinedPosition; } } FL_OPTIMIZATION_LEVEL_O0_END void fillFrameBufferNoise() { // Get current UI values uint8_t noise_scale = noiseScale.value(); uint8_t noise_speed = noiseSpeed.value(); // Derive noise coordinates from current time instead of forward iteration uint32_t now = millis(); uint16_t noise_z = now * noise_speed / 10; // Primary time dimension uint16_t noise_x = now * noise_speed / 80; // Slow drift in x uint16_t noise_y = now * noise_speed / 160; // Even slower drift in y (opposite direction) int width = frameBufferPtr->width(); int height = frameBufferPtr->height(); // Data smoothing for low speeds (from NoisePlusPalette example) uint8_t dataSmoothing = 0; if(noise_speed < 50) { dataSmoothing = 200 - (noise_speed * 4); } // Generate noise for each pixel in the frame buffer using cylindrical mapping for(int x = 0; x < width; x++) { for(int y = 0; y < height; y++) { // Convert rectangular coordinates to cylindrical coordinates // Map x to angle (0 to 2*PI), y remains as height float angle = (float(x) / float(width)) * 2.0f * PI; // Convert cylindrical coordinates to cartesian for noise sampling // Use the noise_scale to control the cylinder size in noise space float cylinder_radius = noise_scale; // Use the existing noise_scale parameter // Calculate cartesian coordinates on the cylinder surface float noise_x_cyl = cos(angle) * cylinder_radius; float noise_y_cyl = sin(angle) * cylinder_radius; float noise_z_height = float(y) * noise_scale; // Height component // Apply time-based offsets int xoffset = int(noise_x_cyl) + noise_x; int yoffset = int(noise_y_cyl) + noise_y; int zoffset = int(noise_z_height) + noise_z; // Generate 8-bit noise value using 3D Perlin noise with cylindrical coordinates uint8_t data = inoise8(xoffset, yoffset, zoffset); // Expand the range from ~16-238 to 0-255 (from NoisePlusPalette) data = qsub8(data, 16); data = qadd8(data, scale8(data, 39)); // Apply data smoothing if enabled if(dataSmoothing) { CRGB oldColor = frameBufferPtr->at(x, y); uint8_t olddata = (oldColor.r + oldColor.g + oldColor.b) / 3; // Simple brightness extraction uint8_t newdata = scale8(olddata, dataSmoothing) + scale8(data, 256 - dataSmoothing); data = newdata; } // Map noise to color using palette (adapted from NoisePlusPalette) uint8_t index = data; uint8_t bri = data; // Add color cycling if enabled - also derive from time uint8_t ihue = 0; if(colorLoop) { ihue = (now / 100) % 256; // Derive hue from time instead of incrementing index += ihue; } // Enhance brightness (from NoisePlusPalette example) // if(bri > 127) { // //bri = 255; // } else { // //bri = dim8_raw(bri * 2); // } // Get color from palette and set pixel CRGB color = ColorFromPalette(noisePalette, index, bri); // Apply color boost using ease functions EaseType sat_ease = getEaseType(saturationFunction.value()); EaseType lum_ease = getEaseType(luminanceFunction.value()); color = color.colorBoost(sat_ease, lum_ease); frameBufferPtr->at(x, y) = color; } } } void drawNoise(uint32_t now) { FL_UNUSED(now); fillFrameBufferNoise(); } void draw(float pos) { if (splatRendering) { Tile2x2_u8_wrap pos_tile = corkscrew.at_wrap(pos); //FL_WARN("pos_tile: " << pos_tile); CRGB color = CRGB::Blue; // Apply color boost using ease functions EaseType sat_ease = getEaseType(saturationFunction.value()); EaseType lum_ease = getEaseType(luminanceFunction.value()); color = color.colorBoost(sat_ease, lum_ease); // Draw each pixel in the 2x2 tile using the new wrapping API for (int dx = 0; dx < 2; ++dx) { for (int dy = 0; dy < 2; ++dy) { Tile2x2_u8_wrap::Entry data = pos_tile.at(dx, dy); vec2 wrapped_pos = data.first; // Already wrapped position uint8_t alpha = data.second; // Alpha value if (alpha > 0) { // Only draw if there's some alpha CRGB c = color; c.nscale8(alpha); // Scale the color by the alpha value frameBufferPtr->at(wrapped_pos.x, wrapped_pos.y) = c; } } } } else { // None splat rendering, looks aweful. vec2f pos_vec2f = corkscrew.at_no_wrap(pos); vec2 pos_i16 = vec2(pos_vec2f.x, pos_vec2f.y); CRGB color = CRGB::Blue; // Apply color boost using ease functions EaseType sat_ease = getEaseType(saturationFunction.value()); EaseType lum_ease = getEaseType(luminanceFunction.value()); color = color.colorBoost(sat_ease, lum_ease); // Now map the cork screw position to the cylindrical buffer that we // will draw. frameBufferPtr->at(pos_i16.x, pos_i16.y) = color; // Draw a blue pixel at (w, h) } } CRGBPalette16 getFirePalette() { int paletteIndex = (int)firePalette.value(); switch (paletteIndex) { case 0: return firepal; case 1: return electricGreenFirePal; case 2: return electricBlueFirePal; default: return firepal; } } uint8_t getFirePaletteIndex(uint32_t millis32, int width, int max_width, int height, int max_height, uint32_t y_speed) { uint16_t scale = fireScaleXY.as(); float xf = (float)width / (float)max_width; uint8_t x = (uint8_t)(xf * 255); uint32_t cosx = cos8(x); uint32_t sinx = sin8(x); float trig_scale = scale * fireScaleX.value(); cosx *= trig_scale; sinx *= trig_scale; uint32_t y = height * scale + y_speed; uint16_t z = millis32 / fireInvSpeedZ.as(); uint16_t noise16 = inoise16(cosx << 8, sinx << 8, y << 8, z << 8); uint8_t noise_val = noise16 >> 8; int8_t subtraction_factor = abs8(height - (max_height - 1)) * 255 / (max_height - 1); return qsub8(noise_val, subtraction_factor); } void fillFrameBufferFire(uint32_t now) { CRGBPalette16 myPal = getFirePalette(); // Calculate the current y-offset for animation (makes the fire move) uint32_t y_speed = now * fireSpeedY.value(); int width = frameBufferPtr->width(); int height = frameBufferPtr->height(); // Loop through every pixel in our cylindrical matrix for (int w = 0; w < width; w++) { for (int h = 0; h < height; h++) { // Calculate which color to use from our palette for this pixel uint8_t palette_index = getFirePaletteIndex(now, w, width, h, height, y_speed); // Get the actual RGB color from the palette CRGB color = ColorFromPalette(myPal, palette_index, 255); // Apply color boost using ease functions EaseType sat_ease = getEaseType(saturationFunction.value()); EaseType lum_ease = getEaseType(luminanceFunction.value()); color = color.colorBoost(sat_ease, lum_ease); // Set the pixel in the frame buffer // Flip coordinates to make fire rise from bottom frameBufferPtr->at((width - 1) - w, (height - 1) - h) = color; } } } void drawFire(uint32_t now) { fillFrameBufferFire(now); } // Wave effect helper functions CRGBPalette16 getWavePalette() { int paletteIndex = (int)wavePalette.value(); switch (paletteIndex) { case 0: return waveBluepal; // Electric blue waves case 1: return waveGreenpal; // Green/red waves case 2: return waveRainbowpal; // Rainbow waves default: return waveBluepal; // Default to blue } } void triggerWaveRipple() { // Create a ripple at a random position within the central area float perc = 0.15f; // 15% margin from edges int width = corkscrew.cylinderWidth(); int height = corkscrew.cylinderHeight(); int min_x = perc * width; int max_x = (1 - perc) * width; int min_y = perc * height; int max_y = (1 - perc) * height; int x = random8(min_x, max_x); int y = random8(min_y, max_y); // Trigger a 2x2 wave ripple for more punch (compensates for blur reduction) float ripple_strength = 1.5f; // Higher value for more impact waveFx->setf(x, y, ripple_strength); waveFx->setf(x + 1, y, ripple_strength); waveFx->setf(x, y + 1, ripple_strength); waveFx->setf(x + 1, y + 1, ripple_strength); FL_WARN("Wave ripple triggered at (" << x << ", " << y << ") with 2x2 pattern"); } void processWaveAutoTrigger(uint32_t now) { // Handle automatic wave triggering if (waveAutoTrigger.value()) { if (now >= nextWaveTrigger) { triggerWaveRipple(); // Calculate next trigger time based on speed float speed = 1.0f - waveTriggerSpeed.value(); uint32_t min_interval = (uint32_t)(500 * speed); // Minimum 500ms * speed uint32_t max_interval = (uint32_t)(3000 * speed); // Maximum 3000ms * speed // Ensure valid range uint32_t min = MIN(min_interval, max_interval); uint32_t max = MAX(min_interval, max_interval); if (min >= max) max = min + 1; nextWaveTrigger = now + random16(min, max); } } } void drawWave(uint32_t now) { // Update wave parameters from UI waveFx->setSpeed(waveSpeed.value()); waveFx->setDampening(waveDampening.value()); waveFx->setHalfDuplex(waveHalfDuplex.value()); waveFx->setXCylindrical(true); // Always keep cylindrical for corkscrew // Update wave color palette CRGBPalette16 currentPalette = getWavePalette(); crgMap->setGradient(currentPalette); // Apply blur settings to the wave blend (for smoother wave effects) waveBlend->setGlobalBlurAmount(waveBlurAmount.value()); waveBlend->setGlobalBlurPasses(waveBlurPasses.value()); // Check if manual trigger button was pressed if (waveTriggerButton.value()) { triggerWaveRipple(); } // Handle auto-triggering processWaveAutoTrigger(now); // Draw the wave effect directly to the frame buffer // Create a DrawContext for the wave renderer Fx::DrawContext waveContext(now, frameBufferPtr->data()); waveBlend->draw(waveContext); } void drawAnimartrix(uint32_t now) { // Update animartrix parameters from UI fxEngine->setSpeed(animartrixTimeSpeed.value()); // Handle animation index changes static int lastAnimartrixIndex = -1; if (animartrixIndex.value() != lastAnimartrixIndex) { lastAnimartrixIndex = animartrixIndex.value(); animartrix->fxSet(animartrixIndex.value()); } // Draw the animartrix effect directly to the frame buffer CRGB* dst = corkscrew.rawData(); fxEngine->draw(now, dst); } void loop() { delay(4); uint32_t now = millis(); frameBufferPtr->clear(); if (allWhite) { CRGB whiteColor = CRGB(8, 8, 8); for (u32 x = 0; x < frameBufferPtr->width(); x++) { for (u32 y = 0; y < frameBufferPtr->height(); y++) { frameBufferPtr->at(x, y) = whiteColor; } } } // Update the corkscrew mapping with auto-advance or manual position control float combinedPosition = get_position(now); float pos = combinedPosition * (corkscrew.size() - 1); if (renderModeDropdown.value() == "Noise") { drawNoise(now); } else if (renderModeDropdown.value() == "Fire") { drawFire(now); } else if (renderModeDropdown.value() == "Wave") { drawWave(now); } else if (renderModeDropdown.value() == "Animartrix") { drawAnimartrix(now); } else { draw(pos); } // Use the new readFrom workflow: // 1. Read directly from the frameBuffer Grid into the corkscrew's internal buffer // use_multi_sampling = true will use multi-sampling to sample from the source grid, // this will give a little bit better accuracy and the screenmap will be more accurate. const bool use_multi_sampling = splatRendering; // corkscrew.readFrom(frameBuffer, use_multi_sampling); corkscrew.draw(use_multi_sampling); // The corkscrew's buffer is now populated and FastLED will display it directly FastLED.setBrightness(brightness.value()); FastLED.show(); }