diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7a9ff5ad78..c72c4a2355 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -11034,6 +11034,247 @@ void mode_slow_transition(void) { } static const char _data_FX_MODE_SLOW_TRANSITION[] PROGMEM = "Slow Transition@Time (min),,,,,,Sweep;!;!;1;pal=2,sx=0,ix=0"; +//////////////////////// +// Liquid // +//////////////////////// +/* + * Liquid: a calm, waving effect for 1-D strips. The field is the amplitude- + * weighted sum of up to 16 travelling sine waves (of decaying amplitude, + * rising frequency and alternating direction), rendered into the Primary + * and Secondary color wells (even / odd-indexed waves). + * + * Additionally, a 'shimmer' effect takes place within a configurable band + * around a configurable wave height, mimicking 'catching the light' of the + * wave and blending the pixel toward the Tertiary color. + * + * Sliders (0..255; "Shimmer Width" uses the 0..31 slider on WLED 17): + * 0 Speed wave motion (0 = frozen in place) + * 1 Waves number of active waves (byte / 16) + * 2 Wavelength spatial scale (0 = tiny waves; 255 = enormous but flat waves) + * 3 Shimmer Height wave height around which shimmer occurs + * 4 Shimmer Width band half-width around the shimmer height + * + * Colors: + * Primary even-indexed waves + * Secondary odd-indexed waves + * Tertiary shimmer / "caught light" color + * + * Wave table: + * Amplitudes are stored in hundredths. + * Frequencies are stored in millihertz. + * Phases use 16-bit binary angles, where 65536 represents one full turn. + * + * AI: below section was generated by an AI + */ + +void mode_liquid(void) { + if (SEGLEN <= 1) FX_FALLBACK_STATIC; + + unsigned len = SEGLEN; + + uint8_t waves = SEGMENT.intensity / 16 + 1; // "Waves": 1-16 + + // Original spatial scale: + // 5.0 - custom1 / 51.0 = (255 - custom1) / 51 + uint8_t wavelength = 255 - SEGMENT.custom1; + + // Both use the natural 8-bit brightness range. + uint8_t shimmerCenter = SEGMENT.custom2; + + // 0.30 * custom3 / 31, represented as 0..77. + uint8_t shimmerWidth = ((uint16_t)SEGMENT.custom3 * 77U) / 31U; + + // Tertiary color is used for shimmer. + uint32_t tertiary = SEGCOLOR(2); + uint8_t shimmerColor[4] = { + (uint8_t)(tertiary >> 24), + (uint8_t)(tertiary >> 16), + (uint8_t)(tertiary >> 8), + (uint8_t)tertiary + }; + + uint16_t speedScale = (uint16_t)SEGMENT.speed * 2U; + + uint32_t timeQuotient = (strip.now / 255U) % 1000000U; + uint8_t timeRemainder = strip.now % 255U; + + uint32_t scaledTime = + (timeQuotient * speedScale + (uint16_t)timeRemainder * speedScale / 255U) % 1000000U; + + /* + * Precompute all values that depend on the wave index but not on + * the pixel position. + */ + uint16_t wavePhaseOffsets[16]; + uint8_t waveColors[16][4]; + uint8_t waveAmplitudes[16]; + + // Sum of the relative amplitudes of all active waves. + uint8_t maxAmplitudeSum = 0; + + // Pre-compute wave characteristics to avoid the same calculations for each pixel + for (uint8_t k = 0; k < waves; k++) { + // Increase temporal frequency non-linearly to avoid harmonically aligned waves. + uint16_t frequency = 80U + 70U * k + 3U * k * k; + uint32_t frequencyPosition = (scaledTime * frequency) % 1000000U; + uint16_t temporalPhase = (frequencyPosition * 4096U) / 62500U; + + /* + * Combine the temporal phase, travel direction and fixed phase + * offset into one value per wave. + */ + // Offset successive waves by the golden angle to avoid regular phase alignment. + uint16_t phaseOffset = (uint16_t)((uint32_t)k * 40503U); + wavePhaseOffsets[k] = (k & 1) ? phaseOffset - temporalPhase : phaseOffset + temporalPhase; + + /* + * Distribute the waves evenly over the complete palette: + * + * 1 wave -> 0 + * 2 waves -> 0, 255 + * 3 waves -> 0, 128, 255 + * 4 waves -> 0, 85, 170, 255 + * + * The result depends only on the wave index and number of waves, + * so calculate the palette color once per frame. + */ + uint8_t paletteIndex = 0; + if (waves > 1) { + paletteIndex = ((uint16_t)k * 255U + (waves - 1U) / 2U) / (waves - 1U); + } + + uint32_t color = SEGMENT.color_from_palette(paletteIndex, false, true, 255); + + for (uint8_t channel = 0; channel < 4; channel++) { + waveColors[k][channel] = color >> (24U - channel * 8U); + } + + uint8_t amplitude = 70U / (k + 2U); + waveAmplitudes[k] = amplitude; + maxAmplitudeSum += amplitude; + } + + if (maxAmplitudeSum == 0) { + maxAmplitudeSum = 1; + } + + /* + * Maximum possible weighted wave amplitude. + */ + uint16_t maxWeightedAmplitude = (uint16_t)maxAmplitudeSum * 255U; + + /* + * Precompute the rounding offset used during normalization. + */ + uint16_t maxWeightedAmplitudeRounding = maxWeightedAmplitude / 2U; + + // Calculate color for all pixels. + for (unsigned px = 0; px < len; px++) { + /* + * Scale the pixel position to one 16-bit turn. + */ + uint16_t position = ((uint32_t)px * 65535U) / len; + + /* + * Calculate the spatial phase increment once per pixel. + * + * Each wave adds this increment, producing: + * + * position * wavelength * (k + 1) + */ + uint32_t spatialIncrement = (uint32_t)position * wavelength; + uint32_t spatialAccumulator = 0; + + /* + * The RGBW accumulators need uint32_t because a color channel + * (0..255) is multiplied by the weighted wave contribution. + */ + uint32_t channels[4] = { 0 }; + + /* + * This is independent of color and represents the total wave + * brightness at this pixel. It is used to control shimmer. + */ + uint16_t totalWaveBrightness = 0; + + for (uint8_t k = 0; k < waves; k++) { + /* + * Spatial frequency increases with wave index. Using an + * accumulator avoids multiplying by (k + 1) inside the loop. + */ + spatialAccumulator += spatialIncrement; + uint16_t spatialPhase = spatialAccumulator >> 8; + + uint16_t phase = spatialPhase + wavePhaseOffsets[k]; + + /* + * Convert sin16_t() from -32768..32767 to brightness 0..255. + */ + uint8_t amplitude = ((int32_t)sin16_t(phase) + 32768) >> 8; + + /* + * Apply this wave's relative amplitude. + */ + uint16_t contribution = (uint16_t)amplitude * waveAmplitudes[k]; + + totalWaveBrightness += contribution; + + /* + * Add this wave's precomputed color contribution directly + * to the pixel. + */ + for (uint8_t channel = 0; channel < 4; channel++) { + channels[channel] += (uint32_t)waveColors[k][channel] * contribution; + } + } + + /* + * Normalize the accumulated colored waves to 8-bit RGBW. + * + * Division happens only once per channel, rather than once + * for every individual wave. + */ + uint8_t color[4]; + for (uint8_t channel = 0; channel < 4; channel++) { + color[channel] = (channels[channel] + maxWeightedAmplitudeRounding) / maxWeightedAmplitude; + } + + /* + * Normalize the underlying wave brightness independently of color. + * Therefore changing the palette does not change where shimmer occurs. + */ + uint8_t pixelBrightness = + ((uint32_t)totalWaveBrightness * 255U + maxWeightedAmplitudeRounding) / maxWeightedAmplitude; + + // Calculate shimmer strength, with a maximum 70% blend (178U). + uint8_t shimmerMix = 0; + + if (shimmerWidth > 0) { + uint8_t brightnessDistance = + (pixelBrightness >= shimmerCenter) ? pixelBrightness - shimmerCenter : shimmerCenter - pixelBrightness; + + if (brightnessDistance < shimmerWidth) { + shimmerMix = ((uint16_t)(shimmerWidth - brightnessDistance) * 178U) / shimmerWidth; + } + } + + /* + * Blend toward the tertiary color where shimmer is active. + */ + if (shimmerMix > 0) { + for (uint8_t channel = 0; channel < 4; channel++) { + color[channel] = + ((uint16_t)color[channel] * (255U - shimmerMix) + (uint16_t)shimmerColor[channel] * shimmerMix) / 255U; + } + } + + SEGMENT.setPixelColor(px, RGBW32(color[1], color[2], color[3], color[0])); + } +} +static const char _data_FX_MODE_LIQUID[] PROGMEM = + "Liquid@Speed,Waves,Wavelength,Shimmer Height,Shimmer Width;,,Shimmer;!;;sx=128,ix=128,c1=128,c2=200,c3=10"; +// AI: end + ////////////////////////////////////////////////////////////////////////////////////////// // mode data static const char _data_RESERVED[] PROGMEM = "RSVD"; @@ -11200,6 +11441,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); addEffect(FX_MODE_SLOW_TRANSITION, &mode_slow_transition, _data_FX_MODE_SLOW_TRANSITION); + addEffect(FX_MODE_LIQUID, &mode_liquid, _data_FX_MODE_LIQUID); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index f77b25ab01..9699f423bb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -369,8 +369,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_COLORCLOUDS 218 #define FX_MODE_SLOW_TRANSITION 219 #define FX_MODE_2DANALOGCLOCK 220 // new in 16.1 +#define FX_MODE_LIQUID 221 -#define MODE_COUNT 221 +#define MODE_COUNT 222 #define TRANSITION_FADE 0x00 // universal