From da726c56e6acc570b09ad600be3fc70f761bec23 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 13 Mar 2024 20:57:15 +0100 Subject: [PATCH 01/21] Squashed commit of FXparticleSystem --- wled00/FX.cpp | 1857 +++++++++++++++++++++++++++++++---- wled00/FX.h | 14 +- wled00/FXparticleSystem.cpp | 883 +++++++++++++++++ wled00/FXparticleSystem.h | 99 ++ 4 files changed, 2645 insertions(+), 208 deletions(-) create mode 100644 wled00/FXparticleSystem.cpp create mode 100644 wled00/FXparticleSystem.h diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 14341f5b99..f2796211bf 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -24,9 +24,12 @@ Modified heavily for WLED */ +// Information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata + #include "wled.h" #include "FX.h" #include "fcn_declare.h" +#include "FXparticleSystem.h" #define IBN 5100 @@ -7878,239 +7881,1669 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +/* + * Particle rotating spray + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ -#endif // WLED_DISABLE_2D +uint16_t mode_particlerotatingspray(void) +{ + if (SEGLEN == 1) + return mode_static(); -////////////////////////////////////////////////////////////////////////////////////////// -// mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } - } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - } else { - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 700; // maximum number of particles +#endif + + const uint8_t numSprays = 8; // maximum number of sprays + + PSparticle *particles; + PSpointsource *spray; + + // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + spray = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = 0; // starting angle + SEGMENT.aux1 = 0xFF; // user check + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); // TODO: how to keep track of options? can use SEGMENT.aux1: change hue to random or rainbow depending on check but need to find out when it changed. + spray[i].source.sat = 255; // set saturation + spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center + spray[i].source.y = (rows * PS_P_RADIUS) / 2; // center + spray[i].source.vx = 0; + spray[i].source.vy = 0; + spray[i].maxLife = 400; + spray[i].minLife = 200; + spray[i].vx = 0; // emitting speed + spray[i].vy = 0; // emitting speed + spray[i].var = 0; // emitting variation + } + } + + // change source emitting color from time to time + if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color and update hue if necessary + { + for (i = 0; i < spraycount; i++) + { + spray[i].source.hue++; // = random8(); //change hue of spray source + } + if (SEGMENT.check1 != SEGMENT.aux1) + { + SEGMENT.aux1 = SEGMENT.check1; + for (i = 0; i < spraycount; i++) + { + if (SEGMENT.check1) // random color is checked + { + spray[i].source.hue = random8(); + } + else + { + uint8_t coloroffset = 0xFF / spraycount; + spray[i].source.hue = coloroffset * i; + } + } + } } -} -void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); + uint8_t percycle = spraycount; // maximum number of particles emitted per cycle + i = 0; + j = random(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + while (i < numParticles) + { + if (particles[i].ttl == 0) // find a dead particle + { + // spray[j].source.hue = random8(); //set random color for each particle (using palette) + Emitter_Fountain_emit(&spray[j], &particles[i]); + j = (j + 1) % spraycount; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } + } + i++; } - // now replace all pre-allocated effects - // --- 1D non-audio effects --- - addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); - addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); - addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); - addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); - addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); - addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); - addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); - addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); - addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); - addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); - addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); - addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); - addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); - addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); - addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); - addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); - addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); - addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); - addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); - addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); - addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); - addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); - addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); - addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); - addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); - addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); - addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); - addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); - addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); - addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); - addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); - addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); - addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); - addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); - addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); - addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); - addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); - addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); - addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); - addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); - addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); - addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); - addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + // calculate angle offset for an even distribution + uint16_t angleoffset = 0xFFFF / spraycount; - addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); - addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); - addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); - addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); - addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); - addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); - addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); - addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); - addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); - addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); - addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); - addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); - addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); - addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); - addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); - addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); - addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); - addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); - addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); - addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); - addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); - addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); - addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); - addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); - addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); - addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); - addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); - addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); - addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); - addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); - addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); - addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); - addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); - addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); - addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); - addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); - addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); - addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); - addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); - addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); - addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + for (i = 0; i < spraycount; i++) + { + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.speed << 2; + else + SEGMENT.aux0 -= SEGMENT.speed << 2; - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); - addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); - addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value + spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) + spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) + spray[i].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + } - // --- 1D audio effects --- - addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); - addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); - addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); - addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); - addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); - addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); - addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); - addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); - addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); - addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); - addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + for (i = 0; i < numParticles; i++) + { + Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + } - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); - addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + SEGMENT.fill(BLACK); // clear the matrix - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); - addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); - addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + // render the particles + ParticleSys_render(particles, numParticles, false, false); - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); - addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=18,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); - addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); - addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); - addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); - addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); +/* + * Particle Fireworks + * Rockets shoot up and explode in a random color, sometimes in a defined pattern + * Uses ranbow palette as default + * by DedeHai (Damian Schneider) + */ - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); +uint16_t mode_particlefireworks(void) +{ - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); + if (SEGLEN == 1) + return mode_static(); - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); - addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // --- 2D effects --- -#ifndef WLED_DISABLE_2D - addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); - addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); - addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); - addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); - addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); - addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); + // particle system box dimensions + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio +#ifdef ESP8266 + const uint32_t numParticles = 250; + const uint8_t MaxNumRockets = 4; +#else + const uint32_t numParticles = 650; + const uint8_t MaxNumRockets = 8; +#endif - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); + PSparticle *particles; + PSpointsource *rockets; - addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); - addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (MaxNumRockets); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed - //non audio - addEffect(FX_MODE_2DDNA, &mode_2Ddna, _data_FX_MODE_2DDNA); - addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); - addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); - addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio + // DEBUG_PRINT(F("particle datasize = ")); + // DEBUG_PRINTLN(dataSize); - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); + rockets = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(rockets + MaxNumRockets); // cast the data array into a particle pointer - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); - addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio - addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); - addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); - addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); + uint32_t i = 0; + uint32_t j = 0; + uint8_t numRockets = 1 + ((SEGMENT.custom3) >> 2); // 1 to 8 - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); - addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); - addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); - addEffect(FX_MODE_2DSWIRL, &mode_2DSwirl, _data_FX_MODE_2DSWIRL); // audio - addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); - addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); - addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numRockets; i++) + { + rockets[i].source.ttl = random8(20 * i); // first rocket starts immediately, others follow soon + rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); + // update particles, create particles + uint8_t circularexplosion = random8(numRockets + 2); // choose a rocket by random (but not every round one will be picked) + uint8_t spiralexplosion = random8(numRockets + 2); + + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint16_t emitparticles; // number of particles to emit for each rocket's state + i = 0; + for (j = 0; j < numRockets; j++) + { + // determine rocket state by its speed: + if (rockets[j].source.vy > 0) + { // moving up, emit exhaust + emitparticles = 1; + } + else if (rockets[j].source.vy < 0) + { // falling down + emitparticles = 0; + } + else + { // speed is zero, explode! + emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) + { + emitparticles >> 3; // emit less particles for circle-explosions + rockets[j].maxLife = 150; + rockets[j].minLife = 120; + rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) + } + } + + uint8_t speed = 3; + uint8_t angle = 0; + if (j == spiralexplosion) + angle = random(8); + + for (i; i < numParticles; i++) + { + if (particles[i].ttl == 0) + { // particle is dead + + if (j == circularexplosion && emitparticles > 2) // do circle emit + { + Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); + + if (angle > 242) // full circle completed, increase speed and reset angle + { + angle += 10; + speed += 6; + rockets[j].source.hue = random8(); // new color for next row + rockets[j].source.sat = random8(); + if (emitparticles > 12) + emitparticles -= 12; // emitted about 12 particles for one circle, ensures no incomplete circles are done + else + emitparticles = 0; + } + + // set angle for next particle + angle += 21; // about 30° + } + else if (j == spiralexplosion && emitparticles > 2) // do spiral emit + { + Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); + emitparticles--; + emitparticles--; // only emit half as many particles as in circle explosion, it gets too huge otherwise + angle += 15; + speed++; + rockets[j].source.hue++; + rockets[j].source.sat = random8(155) + 100; + } + else if (emitparticles > 0) + { + Emitter_Fountain_emit(&rockets[j], &particles[i]); + emitparticles--; + } + else + break; // done emitting for this rocket + } + } + } + + // update particles + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + { + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); + } + } + + // update the rockets, set the speed state + for (i = 0; i < numRockets; i++) + { + if (rockets[i].source.ttl) + { + Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) + } + else if (rockets[i].source.vy > 0) + { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) + rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + rockets[i].source.hue = random8(); // random color + rockets[i].source.sat = random8(100) + 155; + rockets[i].maxLife = 200; + rockets[i].minLife = 50; + rockets[i].source.ttl = random8((255 - SEGMENT.speed)) + 50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds + rockets[i].vx = 0; // emitting speed + rockets[i].vy = 0; // emitting speed + rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + } + else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + { + // reinitialize rocket + rockets[i].source.y = 1; // start from bottom + rockets[i].source.x = (rand() % (PS_MAX_X >> 1)) + (PS_MAX_Y >> 2); // centered half + rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height + rockets[i].source.vx = random8(5) - 2; + rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) + rockets[i].source.sat = 250; + rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[i].maxLife = 30; // exhaust particle life + rockets[i].minLife = 10; + rockets[i].vx = 0; // emitting speed + rockets[i].vy = 0; // emitting speed + rockets[i].var = 6; // speed variation around vx,vy (+/- var/2) + } + } + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; + +/* + * Particle Volcano (gravity spray) + * Particles are sprayed from below, spray moves back and forth if option is set + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlevolcano(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // particle system x dimension + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + + const uint32_t numParticles = 450; + const uint8_t numSprays = 1; + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle + + PSparticle *particles; + PSpointsource *spray; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + spray = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation + spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); + spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + spray[i].source.vx = 0; + spray[i].maxLife = 300; // lifetime in frames + spray[i].minLife = 20; + spray[i].source.collide = true; // seeded particles will collide + spray[i].vx = 0; // emitting speed + spray[i].vy = 20; // emitting speed + // spray.var = 10 + (random8() % 4); + } + } + + // change source emitting color from time to time + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + { + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue++; // = random8(); //change hue of spray source + // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing + if (SEGMENT.check2) // bounce + { + if (spray[i].source.vx > 0) // moving to the right currently + { + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + } + else + { + spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + } + } + else + { // wrap on the right side + spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + if (spray[i].source.x >= PS_MAX_X - 32) + spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) + } + spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward + spray[i].vx = 0; + spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.ttl = 255; // source never dies, replenish its lifespan + } + + i = 0; + j = 0; + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0) // find a dead particle + { + // spray[j].source.hue = random8(); //set random color for each particle (using palette) + Emitter_Fountain_emit(&spray[j], &particles[i]); + j = (j + 1) % numSprays; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } + } + } + } + uint8_t hardness = SEGMENT.custom2; + if (SEGMENT.check3) // collisions enabled + detectCollisions(particles, numParticles, hardness); + + for (i = 0; i < numSprays; i++) + { + Particle_Bounce_update(&spray[i].source, (uint8_t)255); // move the source + } + + for (i = 0; i < numParticles; i++) + { + // set color according to ttl ('color by age') + if (SEGMENT.check1) + particles[i].hue = min((uint16_t)220, particles[i].ttl); + + Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, false, false); + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; + +/* + * Particle Fire + * realistic fire effect using particles. heat based and using perlin-noise for wind + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlefire(void) +{ + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames + const uint32_t numParticles = numFlames * 25; + uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle + PSparticle *particles; + PSpointsource *flames; + + // allocate memory and divide it into proper pointers + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numFlames); + + if (!SEGENV.allocateData(dataSize)) + { + return mode_static(); // allocation failed; //allocation failed + } + + flames = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(flames + numFlames); // cast the data array into a particle pointer + + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise + // make sure all particles start out dead + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + + // initialize the flame sprays + for (i = 0; i < numFlames; i++) + { + flames[i].source.ttl = 0; + flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + // note: other parameters are set when creating the flame (see blow) + } + } + + // update the flame sprays: + for (i = 0; i < numFlames; i++) + { + if (flames[i].source.ttl > 0) + { + flames[i].source.ttl--; + flames[i].source.x += flames[i].source.vx; // move the flame source (if it has x-speed) + } + else // flame source is dead + { + // initialize new flame: set properties of source + // from time to time, chang the flame position + // make some of the flames small and slow to add a bright base + + if (random8(40) == 0) // from time to time, change flame position (about once per second at 40 fps) + { + if (SEGMENT.check1) + { // wrap around in X direction, distribute randomly + flames[i].source.x = random16(PS_MAX_X); + } + else // no X-wrapping + { + flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + } + } + + if (i < (numFlames - (cols >> 1))) + { // all but the last few are normal flames + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear + flames[i].source.vx = 0; // (rand() % 3) - 1; + flames[i].source.vy = 0; + flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 2; + flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) + flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) + flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) + } + else + { // base flames to make the base brighter, flames are slower and short lived + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + flames[i].source.vx = 0; + flames[i].source.vy = 0; // emitter moving speed; + flames[i].source.ttl = random8(25) + 15; // lifetime of one flame + flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 12; + flames[i].vx = 0; // emitting speed, sideways + flames[i].vy = 1 + (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + flames[i].var = 2; // speed variation around vx,vy (+/- var/2) + } + } + } + + SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + + // update particles, create particles + uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0 && percycle > 0) + { + Emitter_Flame_emit(&flames[j], &particles[i]); + j++; + if (j >= numFlames) + { // or simpler: j=j%numFlames; + j = 0; + } + percycle--; + } + else if (particles[i].ttl) + { + // add wind using perlin noise + int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + particles[i].vx = windspeed; + FireParticle_update(&particles[i], SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user + } + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_renderParticleFire(particles, numParticles, SEGMENT.check1); // draw matrix + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; + +/* +particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation +this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particlefall(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint32_t numParticles = 500; + + PSparticle *particles; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + + uint32_t i = 0; + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + } + + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero + { + while (i < numParticles) // emit particles + { + if (particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position just over the top of the matrix + particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner + + if (random8(5) == 0) // 16% of particles apper anywhere + particles[i].x = random16(cols * PS_P_RADIUS - 1); + else // rest is emitted at center half + particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); + + particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height + particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider + particles[i].vy = -(SEGMENT.speed >> 1); + particles[i].hue = random8(); // set random color + particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + particles[i].collide = true; // particle will collide + break; // quit loop if all particles of this round emitted + } + i++; + } + } + + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + detectCollisions(particles, numParticles, hardness); + + // now move the particles + for (i = 0; i < numParticles; i++) + { + // apply 'air friction' to smooth things out, slows down all particles depending on their speed, only done on low speeds + if (SEGMENT.speed < 50) + { + applyFriction(&particles[i], 50 - SEGMENT.speed); + } + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; + +/* + * Particle Waterfall + * Uses palette for particle color, spray source at top emitting particles, many config options + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particlewaterfall(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint32_t numParticles = 500; + const uint8_t numSprays = 2; + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle + + PSparticle *particles; + PSpointsource *spray; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + spray = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation + spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2 * PS_P_RADIUS * (i); + spray[i].source.y = (rows + 4) * (PS_P_RADIUS * (i + 1)); // source y position, few pixels above the top to increase spreading before entering the matrix + spray[i].source.vx = 0; + spray[i].source.collide = true; // seeded particles will collide + spray[i].maxLife = 600; // lifetime in frames + spray[i].minLife = 200; + spray[i].vx = 0; // emitting speed + spray[i].var = 7; // emiting variation + } + } + + // change source emitting color + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue++; // change hue of spray source + } + + uint8_t intensity = SEGMENT.intensity; + + if (SEGMENT.call % (9 - (intensity >> 5)) == 0 && intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + { + + for (i = 0; i < numSprays; i++) + { + spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + spray[i].source.ttl = 255; // source never dies, replenish its lifespan + spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 + } + + i = 0; + j = 0; + while (i < numParticles) + { + if (particles[i].ttl == 0) // find a dead particle + { + Emitter_Fountain_emit(&spray[j], &particles[i]); + j = (j + 1) % numSprays; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } + } + i++; + } + } + + // detect and handle collisions + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + + if (SEGMENT.custom2 > 0) // switch off collisions if hardnes is set to zero + { + detectCollisions(particles, numParticles, hardness); + } + else + { + hardness = 150; // set hardness (for ground bounce) to fixed value if not using collisions + } + + // now move the particles + for (i = 0; i < numParticles; i++) + { + // every now and then, apply 'air friction' to smooth things out, slows down all particles a little + if (SEGMENT.call % 8 == 0) + { + applyFriction(&particles[i], 1); + } + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "Particle Waterfall@Particle Speed,Intensity,Speed Variation,Collision Hardness,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=150,ix=240,c1=0,c2=128,c3=17,o1=0,o2=0,o3=1"; + +/* +Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particlebox(void) +{ + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + const uint32_t numParticles = 255; // maximum number of particles + + PSparticle *particles; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = rand(); // position (either in noise or in sine function) + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) + particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) + particles[i].sat = 255; // set full saturation (lets palette choose the color) + particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color + particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder + particles[i].collide = true; // all particles collide + } + } + + uint16_t displayparticles = SEGMENT.intensity; + + i = 0; + j = 0; + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting + { + + int32_t xgravity; + int32_t ygravity; + uint8_t scale; + + SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise + + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); + if (SEGMENT.check1) // sloshing, y force is alwys downwards + { + if (ygravity > 0) + ygravity = -ygravity; + } + + // scale the gravity force down + xgravity /= 16; + ygravity /= 16; + Serial.print(xgravity); + Serial.print(" "); + Serial.println(ygravity); + + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl > 0) + { + particles[i].vx += xgravity; + particles[i].vy += ygravity; + particles[i].ttl = 500; // particles never die + } + } + } + + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + detectCollisions(particles, displayparticles, hardness); + + // now move the particles + for (i = 0; i < displayparticles; i++) + { + particles[i].ttl = 500; // particles never die + // every now and then, apply 'air friction' to smooth things out, slows down all particles a little + if (SEGMENT.call % 8 == 0) + { + applyFriction(&particles[i], 1); + } + Particle_Bounce_update(&particles[i], hardness); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, displayparticles, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; + +/* +perlin noise 'gravity' mapping as in particles on noise hills viewed from above +calculates slope gradient at the particle positions +restults in a fuzzy perlin noise display +*/ + +uint16_t mode_particleperlin(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +#ifdef ESP8266 + const uint32_t numParticles = 150; +#else + const uint32_t numParticles = 350; +#endif + + PSparticle *particles; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + SEGMENT.aux0 = rand(); + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = random16(500) + 200; + particles[i].x = random16(cols * PS_P_RADIUS); + particles[i].y = random16(rows * PS_P_RADIUS); + particles[i].sat = 255; // full saturation, color set by palette + } + } + + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); + + // apply 'gravity' from a 2D perlin noise map + SEGMENT.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + + // update position in noise + for (i = 0; i < displayparticles; i++) + { + + if (particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + { + particles[i].ttl = random16(500) + 200; + particles[i].x = random16(cols * PS_P_RADIUS); + particles[i].y = random16(rows * PS_P_RADIUS); + } + int32_t xnoise = particles[i].x / (1 + (SEGMENT.custom3 >> 1)); // position in perlin noise, scaled by slider + int32_t ynoise = particles[i].y / (1 + (SEGMENT.custom3 >> 1)); + + int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position + particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic + { + int16_t xslope = baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0); + int16_t yslope = baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0); + + particles[i].vx += xslope >> 1; // slope is about 0-8 + particles[i].vy += yslope >> 1; + } + } + uint8_t hardness = SEGMENT.custom1; // how hard the collisions are, 255 = full hard. + if (SEGMENT.check1) // collisions enabled + { + detectCollisions(particles, displayparticles, hardness); + } + + // move particles + for (i = 0; i < displayparticles; i++) + { + // apply a bit of friction so particles are less jittery + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) // need to apply friction very rarely or particles will clump + applyFriction(&particles[i], 1); + + Particle_Bounce_update(&particles[i], hardness); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, displayparticles, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,Collision Hardness,Friction,Scale,Collisions;;!;012;pal=54,sx=70;ix=200,c1=190,c2=120,c3=4,o1=0"; + +/* + * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with + * by DedeHai (Damian Schneider) + */ + +uint16_t mode_particleimpact(void) +{ + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + +#ifdef ESP8266 + const uint32_t numParticles = 250; + const uint8_t MaxNumMeteors = 4; +#else + const uint32_t numParticles = 550; + const uint8_t MaxNumMeteors = 8; +#endif + + PSparticle *particles; + PSpointsource *meteors; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (MaxNumMeteors); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + meteors = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(meteors + MaxNumMeteors); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < MaxNumMeteors; i++) + { + meteors[i].source.y = 10; + meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors + meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + meteors[i].source.sat = 255; // full saturation, color chosen by palette + } + } + + // update particles, create particles + + uint32_t emitparticles; // number of particles to emit for each rocket's state + i = 0; + for (j = 0; j < numMeteors; j++) + { + // determine meteor state by its speed: + if (meteors[j].source.vy < 0) // moving down, emit sparks + { + emitparticles = 2; + } + else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' + { + emitparticles = 0; + } + else // speed is zero, explode! + { + meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + } + + for (i; i < numParticles; i++) + { + if (particles[i].ttl == 0) // particle is dead + { + if (emitparticles > 0) + { + Emitter_Fountain_emit(&meteors[j], &particles[i]); + emitparticles--; + } + else + break; // done emitting for this meteor + } + } + } + + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision + + if (SEGMENT.check3) // use collisions if option is set + { + detectCollisions(particles, numParticles, hardness); + } + // update particles + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + { + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) + { + if (meteors[i].source.ttl) + { + Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) + if (meteors[i].source.vy > 0) + meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + { + meteors[i].source.vy = 0; // set speed zero so it will explode + meteors[i].source.vx = 0; + meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame + meteors[i].source.collide = true; // explosion particles will collide if checked + meteors[i].maxLife = 200; + meteors[i].minLife = 50; + meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].vx = 0; // emitting speed x + meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + } + } + else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + { + // reinitialize meteor + meteors[i].source.y = PS_MAX_Y + (PS_P_RADIUS << 2); // start 4 pixels above the top + meteors[i].source.x = random16(PS_MAX_X); + meteors[i].source.vy = -random(30) - 30; // meteor downward speed + meteors[i].source.vx = random8(30) - 15; + meteors[i].source.hue = random8(); // random color + meteors[i].source.ttl = 1000; // long life, will explode at bottom + meteors[i].source.collide = false; // trail particles will not collide + meteors[i].maxLife = 60; // spark particle life + meteors[i].minLife = 20; + meteors[i].vx = 0; // emitting speed + meteors[i].vy = -9; // emitting speed (down) + meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) + } + } + SEGMENT.fill(BLACK); // clear the matrix + // render the particles + ParticleSys_render(particles, numParticles, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=35,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; + +/* +Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles +uses inverse square law like in planetary motion +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleattractor(void) +{ + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // particle system box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 300; // maximum number of particles +#endif + + PSparticle *particles; + PSparticle *attractor; + PSpointsource *spray; + uint8_t *counters; // counters for the applied force + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * (numParticles + 1); + dataSize += sizeof(uint8_t) * numParticles; + dataSize += sizeof(PSpointsource); + + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed + // divide and cast the data array into correct pointers + particles = reinterpret_cast(SEGENV.data); + attractor = reinterpret_cast(particles + numParticles + 1); + spray = reinterpret_cast(attractor + 1); + counters = reinterpret_cast(spray + 1); + + uint32_t i; + uint32_t j; + + if (SEGMENT.call == 0) // initialization + { + attractor->vx = 0; + attractor->vy = 0; + attractor->x = PS_MAX_X >> 1; // center + attractor->y = PS_MAX_Y >> 1; + + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + + spray->source.hue = random8(); + spray->source.sat = 255; // full saturation, color by palette + spray->source.x = 0; + spray->source.y = 0; + spray->source.vx = random8(5) + 2; + spray->source.vy = random8(4) + 1; + spray->source.ttl = 100; + spray->source.collide = true; // seeded particles will collide (if checked) + spray->maxLife = 300; // seeded particle lifetime in frames + spray->minLife = 30; + spray->vx = 0; // emitting speed + spray->vy = 0; // emitting speed + spray->var = 6; // emitting speed variation + } + + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles); + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + i = 0; + j = 0; + + if (hardness > 1) // enable collisions + { + detectCollisions(particles, displayparticles, hardness); + } + + if (SEGMENT.call % 5 == 0) + { + spray->source.hue++; + spray->source.ttl = 100; // spray never dies + } + + uint8_t emit = 1; // number of particles emitted per frame + Particle_Bounce_update(&spray->source, 255); // bounce the spray around + + SEGMENT.aux0++; // emitting angle + + // now move the particles + for (i = 0; i < displayparticles; i++) + { + + if (particles[i].ttl == 0 && emit--) // find a dead particle + { + if (SEGMENT.call % 2 == 0) // alternate direction of emit + Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); + else + Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0 + 128, SEGMENT.custom1 >> 4); // emit at 180° as well + } + + // every now and then, apply 'air friction' to smooth things out, slows down all particles a little + if (SEGMENT.custom3 > 0) + { + if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) + { + applyFriction(&particles[i], 4); + } + } + + Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); + if (SEGMENT.check1) + Particle_Bounce_update(&particles[i], hardness); + else + Particle_Move_update(&particles[i]); + } + + if (SEGMENT.check2) + SEGMENT.fadeToBlackBy(20); // fade the matrix + else + SEGMENT.fill(BLACK); // clear the matrix + + // ParticleSys_render(&attract, 1, 30, false, false); // render attractor + // render the particles + ParticleSys_render(particles, displayparticles, false, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; + +/* +Particle Spray, just a simple spray animation with many parameters +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particlespray(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // particle system x dimension + const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + + const uint32_t numParticles = 450; + const uint8_t numSprays = 1; + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle + + PSparticle *particles; + PSpointsource *spray; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSpointsource) * (numSprays); + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + spray = reinterpret_cast(SEGENV.data); + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue = random8(); + spray[i].source.sat = 255; // set full saturation + spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); + spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. + spray[i].source.vx = 0; + spray[i].maxLife = 300; // lifetime in frames + spray[i].minLife = 20; + spray[i].source.collide = true; // seeded particles will collide + spray[i].vx = 0; // emitting speed + spray[i].vy = 0; // emitting speed + spray[i].var = 10; + } + } + + // change source emitting color from time to time + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + { + for (i = 0; i < numSprays; i++) + { + spray[i].source.hue++; // = random8(); //change hue of spray source + // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PS_MAX_X); + spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PS_MAX_Y); + } + + i = 0; + j = 0; + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0) // find a dead particle + { + // spray[j].source.hue = random8(); //set random color for each particle (using palette) + Emitter_Angle_emit(&spray[j], &particles[i], SEGMENT.custom3 << 3, SEGMENT.speed >> 2); + j = (j + 1) % numSprays; + if (percycle-- == 0) + { + break; // quit loop if all particles of this round emitted + } + } + } + } + + uint8_t hardness = 200; + + if (SEGMENT.check3) // collisions enabled + detectCollisions(particles, numParticles, hardness); + + for (i = 0; i < numParticles; i++) + { + // particles[i].hue = min((uint16_t)220, particles[i].ttl); + if (SEGMENT.check1) // use gravity + Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); + else // bounce particles + { + if (SEGMENT.check2) // wrap x + Particle_Move_update(&particles[i], true, true, false); + else // bounce + Particle_Bounce_update(&particles[i], hardness); + } + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check2, false); + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; + +#endif // WLED_DISABLE_2D + + +////////////////////////////////////////////////////////////////////////////////////////// +// mode data +static const char _data_RESERVED[] PROGMEM = "RSVD"; + +// add (or replace reserved) effect mode and data into vector +// use id==255 to find unallocated gaps (with "Reserved" data string) +// if vector size() is smaller than id (single) data is appended at the end (regardless of id) +void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { + if (id == 255) { // find empty slot + for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + } + if (id < _mode.size()) { + if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect + _mode[id] = mode_fn; + _modeData[id] = mode_name; + } else { + _mode.push_back(mode_fn); + _modeData.push_back(mode_name); + if (_modeCount < _mode.size()) _modeCount++; + } +} + +void WS2812FX::setupEffectData() { + // Solid must be first! (assuming vector is empty upon call to setup) + _mode.push_back(&mode_static); + _modeData.push_back(_data_FX_MODE_STATIC); + // fill reserved word in case there will be any gaps in the array + for (size_t i=1; i<_modeCount; i++) { + _mode.push_back(&mode_static); + _modeData.push_back(_data_RESERVED); + } + // now replace all pre-allocated effects + // --- 1D non-audio effects --- + addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); + addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); + addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); + addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); + addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); + addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); + addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); + addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); + addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); + addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); + addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); + addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); + addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); + addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); + addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); + addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); + addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); + addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); + addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); + addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); + addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); + addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); + addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); + addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); + addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); + addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); + addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); + addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); + addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); + addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); + addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); + addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); + addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); + addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); + addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); + addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); + addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); + addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); + addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); + addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + + addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); + addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); + addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); + addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + + addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); + addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); + addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); + addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); + addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); + addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); + addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); + addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); + addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); + addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); + addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); + addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); + addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); + addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); + addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); + addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); + addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); + addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); + addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); + addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); + addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); + addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); + addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); + addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); + addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); + addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); + addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); + addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); + addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); + addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); + addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); + addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); + addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); + addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); + addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); + addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); + addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); + addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + + addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); + addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); + addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + + // --- 1D audio effects --- + addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); + addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); + addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); + addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); + addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); + addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); + addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); + addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); + addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); + addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); + addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + + addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); + addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + + addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); + addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); + addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + + addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); + addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + + addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); + addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); + addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); + addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); + addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); + + addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); + + addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); + + addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); + addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + + // --- 2D effects --- +#ifndef WLED_DISABLE_2D + addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); + addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); + addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); + addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); + addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); + + addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio + + addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); + + addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); + addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); + + //non audio + addEffect(FX_MODE_2DDNA, &mode_2Ddna, _data_FX_MODE_2DDNA); + addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); + addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); + addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio + + addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); + + addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); + addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio + addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); + addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); + addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); + + addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); + addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); + addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); + addEffect(FX_MODE_2DSWIRL, &mode_2DSwirl, _data_FX_MODE_2DSWIRL); // audio + addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); + addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); + addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); + + addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); addEffect(FX_MODE_2DBLACKHOLE, &mode_2DBlackHole, _data_FX_MODE_2DBLACKHOLE); @@ -8119,6 +9552,18 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + + addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); + addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); + addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index 3aa19bc357..d35c29ea17 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -318,8 +318,18 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_PARTICLEVOLCANO 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEROTATINGSPRAY 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEFALL 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEWATERFALL 196 +#define FX_MODE_PARTICLESPRAY 197 +#define MODE_COUNT 198 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 0000000000..ab3a911af1 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,883 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys + + LICENSE + The MIT License (MIT) + Copyright (c) 2024 Damian Schneider + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + +/* + Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ + it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit + this should be used to optimize speed but not if memory is affected much +*/ + +#include "FXparticleSystem.h" +#include "wled.h" +#include "FastLED.h" +#include "FX.h" + +// Fountain style emitter for particles used for flames (particle TTL depends on source TTL) +void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) +{ + part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); + part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL + part->hue = emitter->source.hue; + //part->sat = emitter->source.sat; //flame does not use saturation +} + +// fountain style emitter +void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) +{ + part->x = emitter->source.x; // + random8(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. + part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; + part->hue = emitter->source.hue; + part->sat = emitter->source.sat; + part->collide = emitter->source.collide; +} + +// Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var +void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed) +{ + emitter->vx = (((int16_t)cos8(angle)-127) * speed) >> 7; //cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 + emitter->vy = (((int16_t)sin8(angle)-127) * speed) >> 7; + Emitter_Fountain_emit(emitter, part); +} +// attracts a particle to an attractor particle using the inverse square-law +void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +{ + // Calculate the distance between the particle and the attractor + int dx = attractor->x - particle->x; + int dy = attractor->y - particle->y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy + 1; + if (distanceSquared < 4096) + { + if (swallow) // particle is close, kill it + { + particle->ttl = 0; + return; + } + distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces + } + + int32_t shiftedstrength = (int32_t)strength << 16; + int32_t force; + int32_t xforce; + int32_t yforce; + int32_t xforce_abs; // absolute value + int32_t yforce_abs; + + force = shiftedstrength / distanceSquared; + xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting + yforce = (force * dy) >> 10; + xforce_abs = abs(xforce); // absolute value + yforce_abs = abs(yforce); + + uint8_t xcounter = (*counter) & 0x0F; // lower four bits + uint8_t ycounter = (*counter) >> 4; // upper four bits + + *counter = 0; // reset counter, is set back to correct values below + + // for small forces, need to use a delay timer (counter) + if (xforce_abs < 16) + { + xcounter += xforce_abs; + if (xcounter > 15) + { + xcounter -= 15; + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + // apply force in x direction + if (dx < 0) + { + particle->vx -= 1; + } + else + { + particle->vx += 1; + } + } + else //save counter value + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + } + else + { + particle->vx += xforce >> 4; // divide by 16 + } + + if (yforce_abs < 16) + { + ycounter += yforce_abs; + + if (ycounter > 15) + { + ycounter -= 15; + *counter |= (ycounter << 4) & 0xF0; // write upper four bits + + if (dy < 0) + { + particle->vy -= 1; + } + else + { + particle->vy += 1; + } + } + else // save counter value + *counter |= (ycounter << 4) & 0xF0; // write upper four bits + } + else + { + particle->vy += yforce >> 4; // divide by 16 + } + // TODO: need to limit the max speed? +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) +{ + // Matrix dimension + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + if (part->ttl > 0) + { + // age + part->ttl--; + + // apply velocity + part->x += (int16_t)part->vx; + part->y += (int16_t)part->vy; + + part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + // apply velocity + int32_t newX, newY; + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; + + part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + // x direction, handle wraparound + if (wrapX) + { + newX = newX % (PS_MAX_X + 1); + if (newX < 0) + newX = PS_MAX_X - newX; + } + else if ((part->x <= 0) || (part->x >= PS_MAX_X)) // check if particle is out of bounds + { + if (killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; + } + part->x = newX; // set new position + + if (wrapY) + { + newY = newY % (PS_MAX_Y + 1); + if (newY < 0) + newY = PS_MAX_Y - newY; + } + else if ((part->y <= 0) || (part->y >= PS_MAX_Y)) // check if particle is out of bounds + { + if (killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; + } + part->y = newY; // set new position + } + +} + +// bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) +void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) +{ + // Matrix dimension + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + if (part->ttl > 0) + { + // age + part->ttl--; + + part->outofbounds = 0; // reset out of bounds (particles are never out of bounds) + + // apply velocity + int16_t newX, newY; + + // apply velocity + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; + + if ((newX <= 0) || (newX >= PS_MAX_X)) + { // reached an edge + part->vx = -part->vx; // invert speed + part->vx = (((int16_t)part->vx) * ((int16_t)hardness+1)) >> 8; // reduce speed as energy is lost on non-hard surface + } + + if ((newY <= 0) || (newY >= PS_MAX_Y)) + { // reached an edge + part->vy = -part->vy; // invert speed + part->vy = (((int16_t)part->vy) * ((int16_t)hardness+1)) >> 8; // reduce speed as energy is lost on non-hard surface + } + + newX = max(newX, (int16_t)0); // limit to positive + newY = max(newY, (int16_t)0); + part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries + part->y = min(newY, (int16_t)PS_MAX_Y); + } + +} + +// particle moves, gravity force is applied and ages, if wrapX is set, pixels leaving in x direction reappear on other side, hardness is surface hardness for bouncing (127 means 50% speed lost each bounce) +void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) +{ + // Matrix dimension + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + if (part->ttl > 0) + { + // age + part->ttl--; + + // check if particle is out of bounds or died + if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 2)) + { // if it moves more than 1 pixel below y=0, it will not come back. also remove particles that too far above + part->ttl = 0; + return; // particle died, we are done + } + if (wrapX == false) + { + if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 2)) + { // left and right: keep it alive as long as its not too far out (if adding more effects like wind, it may come back) + part->ttl = 0; + return; // particle died, we are done + } + } + + // apply acceleration (gravity) every other frame, doing it every frame is too strong + if (SEGMENT.call % 2 == 0) + { + if (part->vy > -MAXGRAVITYSPEED) + part->vy = part->vy - 1; + } + + // apply velocity + int16_t newX, newY; + + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; + + part->outofbounds = 0; + // check if particle is outside of displayable matrix + + // x direction, handle wraparound (will overrule bounce x) and bounceX + if (wrapX) + { + newX = newX % (PS_MAX_X + 1); + if (newX < 0) + newX = PS_MAX_X - newX; + } + else + { + if (newX < 0 || newX > PS_MAX_X) + { // reached an edge + if (bounceX) + { + part->vx = -part->vx; // invert speed + part->vx = (((int16_t)part->vx) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface + newX = max(newX, (int16_t)0); // limit to positive + newX = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries + } + else // not bouncing and out of matrix + part->outofbounds = 1; + } + } + + part->x = newX; // set new position + + // y direction, handle bounceY (bounces at ground only) + if (newY < 0) + { // || newY > PS_MAX_Y) { //reached an edge + if (bounceY) + { + part->vy = -part->vy; // invert speed + part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface + newY = max(newY, (int16_t)0); // limit to positive (helps with piling as that can push particles out of frame) + // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + } + else // not bouncing and out of matrix + part->outofbounds = 1; + } + + part->y = newY; // set new position + } +} + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY) +{ +#ifdef ESP8266 + bool fastcoloradd = true; // on ESP8266, we need every bit of performance we can get +#else + bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add +#endif + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + int16_t x, y; + uint8_t dx, dy; + uint32_t intensity; + CRGB baseRGB; + uint32_t i; + uint8_t brightess; // particle brightness, fades if dying + + + // go over particles and update matrix cells on the way + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0 || particles[i].outofbounds) + { + continue; + } + // generate RGB values for particle + brightess = min(particles[i].ttl, (uint16_t)255); + + if (particles[i].sat < 255) + { + CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND)); + baseHSV.s = particles[i].sat; + baseRGB = (CRGB)baseHSV; + } + else + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); + dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); + + x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); + y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + + // for vx=1, vy=1: starts out with all four pixels at the same color (32/32) + // moves to upper right pixel (64/64) + // then moves one physical pixel up and right(+1/+1), starts out now with + // lower left pixel fully bright (0/0) and moves to all four pixel at same + // color (32/32) + + if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + x--; // shift x to next pixel left, will overflow to 255 if 0 + dx = dx + (PS_P_RADIUS >> 1); + } + else // if jump has ocurred + { + dx = dx - (PS_P_RADIUS >> 1); // adjust dx so pixel fades + } + + if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + y--; // shift y to next pixel down, will overflow to 255 if 0 + dy = dy + (PS_P_RADIUS >> 1); + } + else + { + dy = dy - (PS_P_RADIUS >> 1); + } + + if (wrapX) + { // wrap it to the other side if required + if (x < 0) + { // left half of particle render is out of frame, wrap it + x = cols - 1; + } + } + if (wrapY) + { // wrap it to the other side if required + if (y < 0) + { // left half of particle render is out of frame, wrap it + y = rows - 1; + } + } + + // calculate brightness values for all four pixels representing a particle using linear interpolation, + // add color to the LEDs. + // intensity is a scaling value from 0-255 (0-100%) + + // bottom left + if (x < cols && y < rows) + { + // calculate the intensity with linear interpolation + intensity = ((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + // scale the particle base color by the intensity and add it to the pixel + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); + } + // bottom right; + x++; + if (wrapX) + { // wrap it to the other side if required + if (x >= cols) + x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) + } + if (x < cols && y < rows) + { + intensity = ((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); + } + // top right + y++; + if (wrapY) + { // wrap it to the other side if required + if (y >= rows) + y = y % rows; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) + } + if (x < cols && y < rows) + { + intensity = ((uint32_t)dx * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); + } + // top left + x--; + if (wrapX) + { // wrap it to the other side if required + if (x < 0) + { // left half of particle render is out of frame, wrap it + x = cols - 1; + } + } + if (x < cols && y < rows) + { + intensity = ((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); + } + } +} + +// update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true +// particles move upwards faster if ttl is high (i.e. they are hotter) +void FireParticle_update(PSparticle *part, bool wrapX) +{ + // Matrix dimension + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + // particle box dimensions + const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + + if (part->ttl > 0) + { + // age + part->ttl--; + + // apply velocity + part->x = part->x + (int16_t)part->vx; + part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + + part->outofbounds = 0; + // check if particle is out of bounds, wrap around to other side if wrapping is enabled + // x-direction + if ((part->x < 0) || (part->x > PS_MAX_X)) + { + if (wrapX) + { + part->x = part->x % (PS_MAX_X + 1); + if (part->x < 0) + part->x = PS_MAX_X - part->x; + } + else + { + part->ttl = 0; + } + } + + // y-direction + if ((part->y < -(PS_P_RADIUS << 4)) || (part->y > PS_MAX_Y)) + { // position up to 8 pixels below the matrix is allowed, used for wider flames at the bottom + part->ttl = 0; + } + else if (part->y < 0) + { + part->outofbounds = 1; + } + } +} + +// render fire particles to the LED buffer using heat to color +// each particle adds heat according to its 'age' (ttl) which is then rendered to a fire color in the 'add heat' function +void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) +{ + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + int32_t x, y; + uint8_t dx, dy; + uint32_t tempVal; + uint32_t i; + + // go over particles and update matrix cells on the way + // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) + for (i = 0; i < numParticles; i++) + { + if (particles[i].ttl == 0 || particles[i].outofbounds) + { + continue; + } + + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); + dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); + + x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); // compiler should optimize to bit shift + y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + + if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + x--; // shift left + dx = dx + (PS_P_RADIUS >> 1); // add half a radius + } + else // if jump has ocurred, fade pixel + { + // adjust dx so pixel fades + dx = dx - (PS_P_RADIUS >> 1); + } + + if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached + { + y--; // shift row + dy = dy + (PS_P_RADIUS >> 1); + } + else + { + // adjust dy so pixel fades + dy = dy - (PS_P_RADIUS >> 1); + } + + if (wrapX) + { + if (x < 0) + { // left half of particle render is out of frame, wrap it + x = cols - 1; + } + } + + // calculate brightness values for all six pixels representing a particle using linear interpolation + // bottom left + if (x < cols && x >=0 && y < rows && y >=0) + { + tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + // bottom right; + x++; + if (wrapX) + { // wrap it to the other side if required + if (x >= cols) + x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) + } + if (x < cols && y < rows && y >= 0) + { + tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + // top right + y++; + if (x < cols && y < rows) + { + tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + // top left + x--; + if (wrapX) + { // wrap it to the other side if required + if (x < 0) + { // left half of particle render is out of frame, wrap it + x = cols - 1; + } + } + if (x < cols && x >= 0 && y < rows) + { + tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); + PartMatrix_addHeat(x, y, tempVal); + PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + } + } +} + +// adds 'heat' to red color channel, if it overflows, add it to next color channel +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) +{ + + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + + CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) + uint32_t newcolorvalue; + uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) + + // define how the particle TTL value (which is the heat given to the function) maps to heat, if lower, fire is more red, if higher, fire is brighter as bright flames travel higher and decay faster + + heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough + + // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire + uint8_t i = (colormode & 0x07) >> 1; + i = i % 3; + int8_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes + if (currentcolor[i] < 255) + { + newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again + // check if there is heat left over + if (newcolorvalue == 255) + { // there cannot be a leftover if it is not full + heat = heat - (255 - currentcolor[i]); // heat added is difference from current value to full value, subtract it from the inital heat value so heat is the remaining heat not added yet + // this cannot produce an underflow since we never add more than the initial heat value + } + else + { + heat = 0; // no heat left + } + currentcolor[i] = (uint8_t)newcolorvalue; + } + + if (heat > 0) // there is still heat left to be added + { + i += increment; + i = i % 3; + + if (currentcolor[i] < 255) + { + newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again + // check if there is heat left over + if (newcolorvalue == 255) // there cannot be a leftover if red is not full + { + heat = heat - (255 - currentcolor[i]); // heat added is difference from current red value to full red value, subtract it from the inital heat value so heat is the remaining heat not added yet + // this cannot produce an underflow since we never add more than the initial heat value + } + else + { + heat = 0; // no heat left + } + currentcolor[i] = (uint8_t)newcolorvalue; + } + } + if (heat > 0) // there is still heat left to be added + { + i += increment; + i = i % 3; + if (currentcolor[i] < 255) + { + newcolorvalue = currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = min(newcolorvalue, (uint32_t)50); // limit so it does not go full white + currentcolor[i] = (uint8_t)newcolorvalue; + } + } + + SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); +} + +// detect collisions in an array of particles and handle them +void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hardness) +{ + // detect and handle collisions + uint32_t i,j; + int32_t startparticle = 0; + int32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up + + if (SEGMENT.call % 2 == 0) + { // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + startparticle = endparticle; + endparticle = numparticles; + } + + for (i = startparticle; i < endparticle; i++) + { + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].collide && particles[i].outofbounds==0) // if particle is alive and does collide and is not out of view + { + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < numparticles; j++) + { // check against higher number particles + if (particles[j].ttl > 0) // if target particle is alive + { + dx = particles[i].x - particles[j].x; + if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS))) //check x direction, if close, check y direction + { + dy = particles[i].y - particles[j].y; + if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS)) && (dy < (PS_P_HARDRADIUS)) && (dy > (-PS_P_HARDRADIUS))) + { // particles are close + handleCollision(&particles[i], &particles[j], hardness); + } + } + } + } + } + } +} + +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision) +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) +{ + + int32_t dx = particle2->x - particle1->x; + int32_t dy = particle2->y - particle1->y; + int32_t distanceSquared = dx * dx + dy * dy; + + // Calculate relative velocity + int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + + if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + { + // Adjust positions based on relative velocity direction + + if (relativeVx < 0) { //if true, particle2 is on the right side + particle1->x--; + particle2->x++; + } else{ + particle1->x++; + particle2->x--; + } + + if (relativeVy < 0) { + particle1->y--; + particle2->y++; + } else{ + particle1->y++; + particle2->y--; + } + distanceSquared++; + } + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); + + // If particles are moving towards each other + if (dotProduct < 0) + { + const uint8_t bitshift = 14; // bitshift used to avoid floats + + // Calculate new velocities after collision + int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; + int32_t ximpulse = (impulse * dx) >> bitshift; + int32_t yimpulse = (impulse * dy) >> bitshift; + particle1->vx += ximpulse; + particle1->vy += yimpulse; + particle2->vx -= ximpulse; + particle2->vy -= yimpulse; + + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. slow movements are stopped + { + particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; + } + } + + // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle + // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) + // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter + if (distanceSquared < (int32_t)2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) + { + uint8_t choice = random8(2);//randomly choose one particle to push, avoids oscillations + const int32_t HARDDIAMETER = (int32_t)2*PS_P_HARDRADIUS; + + + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) + { // distance is too small, push them apart + + int32_t push; + if (dx <= 0) + push = -1;//-(PS_P_HARDRADIUS + dx); // inverted push direction + else + push = 1;//PS_P_HARDRADIUS - dx; + + if (choice) // chose one of the particles to push, avoids oscillations + particle1->x -= push; + else + particle2->x += push; + } + + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + { + + int32_t push; + if (dy <= 0) + push = -1; //-(PS_P_HARDRADIUS + dy); // inverted push direction + else + push = 1; // PS_P_HARDRADIUS - dy; + + if (choice) // chose one of the particles to push, avoids oscillations + particle1->y -= push; + else + particle2->y += push; + } + //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + } + + +} + +// slow down particle by friction, the higher the speed, the higher the friction +void applyFriction(PSparticle *particle, uint8_t coefficient) +{ + if(particle->ttl) + { + particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; + particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; + } +} diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 0000000000..824acc7b37 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,99 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys + + LICENSE + The MIT License (MIT) + Copyright (c) 2024 Damian Schneider + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*/ + + +#include + +//particle dimensions (subpixel division) +#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement +#define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity +#define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 + + +//todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! +//flags as bitfields is still very fast to access. + +union Flags { + struct { + + }; + uint8_t flagsByte; +}; + + +//struct for a single particle +typedef struct { + int16_t x; //x position in particle system + int16_t y; //y position in particle system + int8_t vx; //horizontal velocity + int8_t vy; //vertical velocity + uint16_t ttl; // time to live + uint8_t hue; // color hue + uint8_t sat; // color saturation + //add a one byte bit field: + bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area + bool collide : 1; //if flag is set, particle will take part in collisions + bool flag2 : 1; // unused flags... could use one for collisions to make those selective. + bool flag3 : 1; + uint8_t counter : 4; //a 4 bit counter for particle control +} PSparticle; + +//struct for a particle source +typedef struct { + uint16_t minLife; //minimum ttl of emittet particles + uint16_t maxLife; //maximum ttl of emitted particles + PSparticle source; //use a particle as the emitter source (speed, position, color) + uint8_t var; //variation of emitted speed + int8_t vx; //emitting speed + int8_t vy; //emitting speed +} PSpointsource; + +#define GRAVITYCOUNTER 2 //the higher the value the lower the gravity (speed is increased every n'th particle update call), values of 1 to 4 give good results +#define MAXGRAVITYSPEED 40 //particle terminal velocity + +/* +//todo: make these local variables +uint8_t vortexspeed; //speed around vortex +uint8_t vortexdirection; //1 or 0 +int8_t vortexpull; //if positive, vortex pushes, if negative it pulls +*/ + +void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); +void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); +void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed); +void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); +void Particle_Move_update(PSparticle *part, bool killoutofbounds = false, bool wrapX = false, bool wrapY = false); +void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); +void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness); +void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); +void FireParticle_update(PSparticle *part, bool wrapX = false); +void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); +void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); +void applyFriction(PSparticle *particle, uint8_t coefficient); From 7abbac5815d3fe5198434e8b2a2d1fd042804636 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 17 Mar 2024 21:59:42 +0100 Subject: [PATCH 02/21] FX update - changed firework exhaust to low saturation - updated rotating particle spray animation --- wled00/FX.cpp | 87 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f2796211bf..6638922a72 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7920,20 +7920,19 @@ uint16_t mode_particlerotatingspray(void) uint32_t i = 0; uint32_t j = 0; - uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + uint8_t spraycount = 1 + (SEGMENT.custom1 >> 5); // number of sprays to display, 1-8 if (SEGMENT.call == 0) // initialization { - SEGMENT.aux0 = 0; // starting angle - SEGMENT.aux1 = 0xFF; // user check + SEGMENT.aux0 = 0; // starting angle + SEGMENT.aux1 = 0x01; // check flags for (i = 0; i < numParticles; i++) { particles[i].ttl = 0; } for (i = 0; i < numSprays; i++) - { - spray[i].source.hue = random8(); // TODO: how to keep track of options? can use SEGMENT.aux1: change hue to random or rainbow depending on check but need to find out when it changed. - spray[i].source.sat = 255; // set saturation + { + spray[i].source.sat = 255; // set saturation spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center spray[i].source.y = (rows * PS_P_RADIUS) / 2; // center spray[i].source.vx = 0; @@ -7945,28 +7944,24 @@ uint16_t mode_particlerotatingspray(void) spray[i].var = 0; // emitting variation } } - - // change source emitting color from time to time - if (SEGMENT.call % ((263 - SEGMENT.intensity) >> 3) == 0) // every nth frame, cycle color and update hue if necessary + + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { + if (SEGMENT.check1) + SEGMENT.aux1 |= 0x01; //set the flag + else + SEGMENT.aux1 &= ~0x01; // clear the flag + for (i = 0; i < spraycount; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source - } - if (SEGMENT.check1 != SEGMENT.aux1) - { - SEGMENT.aux1 = SEGMENT.check1; - for (i = 0; i < spraycount; i++) + if (SEGMENT.check1) // random color is checked { - if (SEGMENT.check1) // random color is checked - { - spray[i].source.hue = random8(); - } - else - { - uint8_t coloroffset = 0xFF / spraycount; - spray[i].source.hue = coloroffset * i; - } + spray[i].source.hue = random8(); + } + else + { + uint8_t coloroffset = 0xFF / spraycount; + spray[i].source.hue = coloroffset * i; } } } @@ -7989,20 +7984,44 @@ uint16_t mode_particlerotatingspray(void) i++; } + //set rotation direction and speed + int32_t rotationspeed = SEGMENT.speed << 2; + bool direction = SEGMENT.check2; + + if (SEGMENT.custom2 > 0) // automatic direction change enabled + { + uint16_t changeinterval = (265 - SEGMENT.custom2); + direction = SEGMENT.aux1 & 0x02; //set direction according to flag + + if (SEGMENT.check3) // random interval + { + changeinterval = 20 + changeinterval + random16(changeinterval); + } + + if (SEGMENT.call % changeinterval == 0) //flip direction on next frame + { + SEGMENT.aux1 |= 0x04; // set the update flag (for random interval update) + if (direction) + SEGMENT.aux1 &= ~0x02; // clear the direction flag + else + SEGMENT.aux1 |= 0x02; // set the direction flag + } + } + + if (direction) + SEGMENT.aux0 += rotationspeed << 2; + else + SEGMENT.aux0 -= rotationspeed << 2; + // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; for (i = 0; i < spraycount; i++) { - if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.speed << 2; - else - SEGMENT.aux0 -= SEGMENT.speed << 2; - // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.custom1) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + spray[i].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } for (i = 0; i < numParticles; i++) @@ -8017,7 +8036,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=18,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Particle Speed,Spray Count,Flip Speed, Nozzle Size,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -8197,7 +8216,7 @@ uint16_t mode_particlefireworks(void) rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[i].source.sat = 250; + rockets[i].source.sat = 30; // low saturation -> exhaust is off-white rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) rockets[i].maxLife = 30; // exhaust particle life rockets[i].minLife = 10; From 6d70b6ab0269b15f2d6e40aafec3bc0fd24537b2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 19 Mar 2024 20:17:13 +0100 Subject: [PATCH 03/21] Cleanup & Bugfixes plus major improvements for ESP8266 -added particle reductions for ESP8266 for all FX -Removed collisions from Particle Perlin Noise FX, slows things down and does not contribute to a better effect experience -lots of optimizations for ESP8266, all FX now work (at least on 160MHz but still slow) -Some bugfixes -removed unused variables to make compiler happy --- wled00/FX.cpp | 223 +++++++++++++++++++++++++++++--------------------- 1 file changed, 131 insertions(+), 92 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6638922a72..85f3c82927 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7894,8 +7894,8 @@ uint16_t mode_particlerotatingspray(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 const uint32_t numParticles = 150; // maximum number of particles @@ -8051,16 +8051,16 @@ uint16_t mode_particlefireworks(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 250; - const uint8_t MaxNumRockets = 4; + const uint32_t numParticles = 100; + const uint8_t MaxNumRockets = 2; #else const uint32_t numParticles = 650; const uint8_t MaxNumRockets = 8; @@ -8084,7 +8084,7 @@ uint16_t mode_particlefireworks(void) uint32_t i = 0; uint32_t j = 0; - uint8_t numRockets = 1 + ((SEGMENT.custom3) >> 2); // 1 to 8 + uint8_t numRockets = min(uint8_t(1 + ((SEGMENT.custom3) >> 2)), MaxNumRockets); // 1 to 8 if (SEGMENT.call == 0) // initialization { @@ -8119,11 +8119,16 @@ uint16_t mode_particlefireworks(void) } else { // speed is zero, explode! + + #ifdef ESP8266 + emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + #else emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + #endif rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) { - emitparticles >> 3; // emit less particles for circle-explosions + emitparticles = emitparticles >> 3; // emit less particles for circle-explosions rockets[j].maxLife = 150; rockets[j].minLife = 120; rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) @@ -8135,7 +8140,7 @@ uint16_t mode_particlefireworks(void) if (j == spiralexplosion) angle = random(8); - for (i; i < numParticles; i++) + while(i < numParticles) { if (particles[i].ttl == 0) { // particle is dead @@ -8162,8 +8167,7 @@ uint16_t mode_particlefireworks(void) else if (j == spiralexplosion && emitparticles > 2) // do spiral emit { Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); - emitparticles--; - emitparticles--; // only emit half as many particles as in circle explosion, it gets too huge otherwise + emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise angle += 15; speed++; rockets[j].source.hue++; @@ -8177,6 +8181,7 @@ uint16_t mode_particlefireworks(void) else break; // done emitting for this rocket } + i++; } } @@ -8247,12 +8252,17 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + // particle system x dimension - const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint32_t numParticles = 450; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles +#else + const uint32_t numParticles = 450; // maximum number of particles +#endif + const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -8315,7 +8325,7 @@ uint16_t mode_particlevolcano(void) else { // wrap on the right side spray[i].source.vx = SEGMENT.speed >> 4; // spray speed - if (spray[i].source.x >= PS_MAX_X - 32) + if (spray[i].source.x >= PS_MAX_X - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward @@ -8364,7 +8374,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Moving Speed,Intensity,Particle Speed,Bouncyness,Nozzle Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; /* * Particle Fire @@ -8377,15 +8387,21 @@ uint16_t mode_particlefire(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + + #ifdef ESP8266 + const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 + const uint32_t numParticles = numFlames * 16; + const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames + #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames const uint32_t numParticles = numFlames * 25; + const uint32_t numNormalFlames = numFlames - (cols >> 1); // number of normal flames, rest of flames are baseflames + #endif + uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle PSparticle *particles; PSpointsource *flames; @@ -8419,6 +8435,8 @@ uint16_t mode_particlefire(void) { flames[i].source.ttl = 0; flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + flames[i].source.vx = 0; // emitter moving speed; + flames[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) } } @@ -8449,23 +8467,19 @@ uint16_t mode_particlefire(void) } } - if (i < (numFlames - (cols >> 1))) - { // all but the last few are normal flames - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame so particles alredy spread a little when the appear - flames[i].source.vx = 0; // (rand() % 3) - 1; - flames[i].source.vy = 0; + flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + + if (i < numNormalFlames) + { // all but the last few are normal flames flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 2; + flames[i].minLife = 3; flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) } else - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame - flames[i].source.vx = 0; - flames[i].source.vy = 0; // emitter moving speed; + { // base flames to make the base brighter, flames are slower and short lived flames[i].source.ttl = random8(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; @@ -8508,7 +8522,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=16,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; /* particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8524,10 +8538,14 @@ uint16_t mode_particlefall(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 500; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif PSparticle *particles; @@ -8596,7 +8614,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=0,c3=20,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=20,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8610,11 +8628,17 @@ uint16_t mode_particlewaterfall(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - const uint32_t numParticles = 500; +#ifdef ESP8266 + const uint32_t numParticles = 100; // maximum number of particles + const uint8_t numSprays = 1; +#else + const uint32_t numParticles = 500; // maximum number of particles const uint8_t numSprays = 2; +#endif + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle PSparticle *particles; @@ -8647,8 +8671,14 @@ uint16_t mode_particlewaterfall(void) spray[i].source.y = (rows + 4) * (PS_P_RADIUS * (i + 1)); // source y position, few pixels above the top to increase spreading before entering the matrix spray[i].source.vx = 0; spray[i].source.collide = true; // seeded particles will collide - spray[i].maxLife = 600; // lifetime in frames - spray[i].minLife = 200; + #ifdef ESP8266 + spray[i].maxLife = 100; // lifetime in frames + spray[i].minLife = 50; + #else + spray[i].maxLife = 400; // lifetime in frames + spray[i].minLife = 150; + #endif + spray[i].vx = 0; // emitting speed spray[i].var = 7; // emiting variation } @@ -8733,10 +8763,14 @@ uint16_t mode_particlebox(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + const uint32_t numParticles = 80; // maximum number of particles +#else const uint32_t numParticles = 255; // maximum number of particles +#endif PSparticle *particles; @@ -8747,7 +8781,6 @@ uint16_t mode_particlebox(void) particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8763,17 +8796,15 @@ uint16_t mode_particlebox(void) } } - uint16_t displayparticles = SEGMENT.intensity; + uint16_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); i = 0; - j = 0; if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { int32_t xgravity; int32_t ygravity; - uint8_t scale; SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise @@ -8788,9 +8819,6 @@ uint16_t mode_particlebox(void) // scale the gravity force down xgravity /= 16; ygravity /= 16; - Serial.print(xgravity); - Serial.print(" "); - Serial.println(ygravity); for (i = 0; i < numParticles; i++) { @@ -8828,9 +8856,10 @@ uint16_t mode_particlebox(void) static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* -perlin noise 'gravity' mapping as in particles on noise hills viewed from above -calculates slope gradient at the particle positions +Perlin Noise 'gravity' mapping as in particles on 'noise hills' viewed from above +calculates slope gradient at the particle positions and applies 'downhill' force restults in a fuzzy perlin noise display +by DedeHai (Damian Schneider) */ uint16_t mode_particleperlin(void) @@ -8839,11 +8868,11 @@ uint16_t mode_particleperlin(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - const uint32_t numParticles = 150; + const uint32_t numParticles = 80; #else const uint32_t numParticles = 350; #endif @@ -8857,7 +8886,6 @@ uint16_t mode_particleperlin(void) particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; if (SEGMENT.call == 0) // initialization { @@ -8900,11 +8928,6 @@ uint16_t mode_particleperlin(void) particles[i].vy += yslope >> 1; } } - uint8_t hardness = SEGMENT.custom1; // how hard the collisions are, 255 = full hard. - if (SEGMENT.check1) // collisions enabled - { - detectCollisions(particles, displayparticles, hardness); - } // move particles for (i = 0; i < displayparticles; i++) @@ -8913,7 +8936,7 @@ uint16_t mode_particleperlin(void) if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) // need to apply friction very rarely or particles will clump applyFriction(&particles[i], 1); - Particle_Bounce_update(&particles[i], hardness); + Particle_Bounce_update(&particles[i], 255); } SEGMENT.fill(BLACK); // clear the matrix @@ -8923,7 +8946,7 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,Collision Hardness,Friction,Scale,Collisions;;!;012;pal=54,sx=70;ix=200,c1=190,c2=120,c3=4,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; /* * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with @@ -8935,16 +8958,16 @@ uint16_t mode_particleimpact(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 250; - const uint8_t MaxNumMeteors = 4; + const uint32_t numParticles = 150; + const uint8_t MaxNumMeteors = 2; #else const uint32_t numParticles = 550; const uint8_t MaxNumMeteors = 8; @@ -8991,7 +9014,11 @@ uint16_t mode_particleimpact(void) // determine meteor state by its speed: if (meteors[j].source.vy < 0) // moving down, emit sparks { + #ifdef ESP8266 + emitparticles = 1; + #else emitparticles = 2; + #endif } else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' { @@ -9000,10 +9027,14 @@ uint16_t mode_particleimpact(void) else // speed is zero, explode! { meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again + #ifdef ESP8266 + emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + #else emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + #endif } - for (i; i < numParticles; i++) + while(i < numParticles) { if (particles[i].ttl == 0) // particle is dead { @@ -9015,6 +9046,7 @@ uint16_t mode_particleimpact(void) else break; // done emitting for this meteor } + i++; } } @@ -9050,7 +9082,11 @@ uint16_t mode_particleimpact(void) meteors[i].source.collide = true; // explosion particles will collide if checked meteors[i].maxLife = 200; meteors[i].minLife = 50; + #ifdef ESP8266 + meteors[i].source.ttl = random8(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #else meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #endif meteors[i].vx = 0; // emitting speed x meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) @@ -9093,14 +9129,14 @@ uint16_t mode_particleattractor(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles + const uint32_t numParticles = 90; // maximum number of particles #else const uint32_t numParticles = 300; // maximum number of particles #endif @@ -9111,9 +9147,9 @@ uint16_t mode_particleattractor(void) uint8_t *counters; // counters for the applied force // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * (numParticles + 1); - dataSize += sizeof(uint8_t) * numParticles; - dataSize += sizeof(PSpointsource); + uint32_t dataSize = sizeof(PSparticle) * (numParticles+1); //space for particles and the attractor + dataSize += sizeof(uint8_t) * numParticles; //space for counters + dataSize += sizeof(PSpointsource); //space for spray if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed @@ -9124,7 +9160,6 @@ uint16_t mode_particleattractor(void) counters = reinterpret_cast(spray + 1); uint32_t i; - uint32_t j; if (SEGMENT.call == 0) // initialization { @@ -9153,10 +9188,9 @@ uint16_t mode_particleattractor(void) spray->var = 6; // emitting speed variation } - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles); + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 1; //TODO: the -1 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; - j = 0; if (hardness > 1) // enable collisions { @@ -9208,12 +9242,12 @@ uint16_t mode_particleattractor(void) SEGMENT.fill(BLACK); // clear the matrix // ParticleSys_render(&attract, 1, 30, false, false); // render attractor - // render the particles + // render the particles ParticleSys_render(particles, displayparticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collision Strength,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=210,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collisions,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; /* Particle Spray, just a simple spray animation with many parameters @@ -9227,13 +9261,18 @@ uint16_t mode_particlespray(void) if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system x dimension - const uint16_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); +#ifdef ESP8266 + const uint32_t numParticles = 80; +#else const uint32_t numParticles = 450; +#endif + const uint8_t numSprays = 1; uint8_t percycle = numSprays; // maximum number of particles emitted per cycle @@ -9293,7 +9332,7 @@ uint16_t mode_particlespray(void) if (particles[i].ttl == 0) // find a dead particle { // spray[j].source.hue = random8(); //set random color for each particle (using palette) - Emitter_Angle_emit(&spray[j], &particles[i], SEGMENT.custom3 << 3, SEGMENT.speed >> 2); + Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); j = (j + 1) % numSprays; if (percycle-- == 0) { @@ -9329,7 +9368,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=180,ix=200,c1=220,c2=30,c3=12,o1=1,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=100,ix=160,c1=100,c2=50,c3=20,o1=0,o2=1,o3=0"; #endif // WLED_DISABLE_2D From eebabb2cce6ec8739cb800022830e90a8ce9d654 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 20 Mar 2024 19:51:29 +0100 Subject: [PATCH 04/21] added particle graphical equalizer (GEQ) --- wled00/FX.cpp | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 3 +- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 85f3c82927..bf348deca1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9370,6 +9370,123 @@ uint16_t mode_particlespray(void) } static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=100,ix=160,c1=100,c2=50,c3=20,o1=0,o2=1,o3=0"; +/* +Particle base Graphical Equalizer +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleGEQ(void) +{ + + if (SEGLEN == 1) + return mode_static(); + + const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + // const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + +#ifdef ESP8266 + const uint32_t numParticles = 150; // maximum number of particles +#else + const uint32_t numParticles = 500; // maximum number of particles +#endif + + PSparticle *particles; + + // allocate memory and divide it into proper pointers, max is 32k for all segments. + uint32_t dataSize = sizeof(PSparticle) * numParticles; + if (!SEGENV.allocateData(dataSize)) + return mode_static(); // allocation failed; //allocation failed + + // calculate the end of the spray data and assign it as the data pointer for the particles: + particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer + uint32_t i; + if (SEGMENT.call == 0) // initialization + { + for (i = 0; i < numParticles; i++) + { + particles[i].ttl = 0; + particles[i].sat = 255; // full color + } + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + // map the bands into 16 positions on x axis, emit some particles according to frequency loudness + // Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 + // in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? + // implement it simply first, then add complexity... need to check what looks good + i = 0; + uint32_t bin; // current bin + uint32_t binwidth = (cols * PS_P_RADIUS - 1) >> 4; // emit poisition variation for one bin (+/-) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (bin = 0; bin < 16; bin++) + { + uint32_t xposition = binwidth * bin + (binwidth >> 1); // emit position according to frequency band + uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9); // emit speed according to loudness of band + emitparticles = 0; + + if (fftResult[bin] > threshold) + { + emitparticles = 1; // + (fftResult[bin]>>6); + } + else if (fftResult[bin] > 0) // band has low volue + { + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (random8() % restvolume == 0) + { + emitparticles = 1; + } + } + + while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority + { + if (particles[i].ttl == 0) // find a dead particle + { + // set particle properties + particles[i].ttl = map(SEGMENT.intensity, 0, 255, emitspeed >> 1, emitspeed + random8(emitspeed)); // set particle alive, particle lifespan is in number of frames + particles[i].x = xposition + random8(binwidth) - (binwidth >> 1); // position randomly, deviating half a bin width + particles[i].y = 0; // start at the bottom + particles[i].vx = random8(SEGMENT.custom1 >> 1) - (SEGMENT.custom1 >> 2); // x-speed variation + particles[i].vy = emitspeed; + particles[i].hue = (bin << 4) + random8(17) - 8; // color from palette according to bin + // particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + emitparticles--; + } + i++; + } + } + + // Serial.println(" "); + + uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. + // detectCollisions(particles, numParticles, hardness); + + // now move the particles + for (i = 0; i < numParticles; i++) + { + particles[i].vy -= (SEGMENT.custom3 >> 3); // apply stronger gravity + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + } + + SEGMENT.fill(BLACK); // clear the matrix + + // render the particles + ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Gravity,Wrap X,Side bounce,Ground bounce;;!;012;pal=54,sx=100,ix=200,c1=0,c2=0,c3=0,o1=0,o2=0,o3=0"; + #endif // WLED_DISABLE_2D @@ -9622,6 +9739,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLEGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + #endif // WLED_DISABLE_2D } diff --git a/wled00/FX.h b/wled00/FX.h index d35c29ea17..f796ffe3cb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -329,7 +329,8 @@ #define FX_MODE_PARTICLEIMPACT 195 #define FX_MODE_PARTICLEWATERFALL 196 #define FX_MODE_PARTICLESPRAY 197 -#define MODE_COUNT 198 +#define FX_MODE_PARTICLEGEQ 198 +#define MODE_COUNT 199 typedef enum mapping1D2D { M12_Pixels = 0, From 6c757573a9c117015503b75af79c7d1215dc7016 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 21 Mar 2024 15:55:40 +0100 Subject: [PATCH 05/21] Particle FX Rename, default parameter tuning, bugfix -Now shorter names, 'PS' in front to filter the list -Tuned default parameters to make them look better by default -Bugfix in particle system (removed duplicate application of velocity) -reduced PS fire RAM usage (less particles, less base flames, no noticeable difference) -some variable renaming --- wled00/FX.cpp | 85 ++++++++++++++++++------------------- wled00/FXparticleSystem.cpp | 6 --- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index bf348deca1..6bc85adaeb 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7882,7 +7882,7 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; /* - * Particle rotating spray + * Particle System Candy (aka rotating sprays) * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -8036,7 +8036,7 @@ uint16_t mode_particlerotatingspray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "Rotating Particle Spray@Rotation Speed,Particle Speed,Spray Count,Flip Speed, Nozzle Size,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rotation Speed,Particle Speed,Arms,Flip Speed,Nozzle,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -8055,8 +8055,8 @@ uint16_t mode_particlefireworks(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); + const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 100; @@ -8217,7 +8217,7 @@ uint16_t mode_particlefireworks(void) { // reinitialize rocket rockets[i].source.y = 1; // start from bottom - rockets[i].source.x = (rand() % (PS_MAX_X >> 1)) + (PS_MAX_Y >> 2); // centered half + rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height rockets[i].source.vx = random8(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) @@ -8237,7 +8237,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "Particle Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; /* * Particle Volcano (gravity spray) @@ -8255,7 +8255,7 @@ uint16_t mode_particlevolcano(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system x dimension - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 100; // maximum number of particles @@ -8325,7 +8325,7 @@ uint16_t mode_particlevolcano(void) else { // wrap on the right side spray[i].source.vx = SEGMENT.speed >> 4; // spray speed - if (spray[i].source.x >= PS_MAX_X - 32) //compiler warning can be ignored, source.x is always > 0 + if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward @@ -8374,7 +8374,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "Particle Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; /* * Particle Fire @@ -8390,7 +8390,7 @@ uint16_t mode_particlefire(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; // particle system box dimensions - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 @@ -8398,8 +8398,8 @@ uint16_t mode_particlefire(void) const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 25; - const uint32_t numNormalFlames = numFlames - (cols >> 1); // number of normal flames, rest of flames are baseflames + const uint32_t numParticles = numFlames * 20; + const uint32_t numNormalFlames = numFlames - (cols/3); // number of normal flames, rest of flames are baseflames #endif uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle @@ -8434,7 +8434,7 @@ uint16_t mode_particlefire(void) for (i = 0; i < numFlames; i++) { flames[i].source.ttl = 0; - flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners flames[i].source.vx = 0; // emitter moving speed; flames[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) @@ -8459,15 +8459,15 @@ uint16_t mode_particlefire(void) { if (SEGMENT.check1) { // wrap around in X direction, distribute randomly - flames[i].source.x = random16(PS_MAX_X); + flames[i].source.x = random16(Max_x); } else // no X-wrapping { - flames[i].source.x = PS_P_RADIUS * 3 + random16(PS_MAX_X - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners } } - flames[i].source.y = -1 * PS_P_RADIUS; // set the source below the frame + flames[i].source.y = -PS_P_RADIUS; // set the source below the frame if (i < numNormalFlames) { // all but the last few are normal flames @@ -8522,12 +8522,12 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "Particle Fire@Speed,Intensity,Base Flames,Wind Speed, Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; /* -particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +PS Hail: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation -this is quite versatile, can be made to look like rain or snow or confetti, flying sparks etc. +this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -8614,7 +8614,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "Falling Particles@Speed,Intensity,Randomness,Collision hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=20,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Hail@Speed,Intensity,Randomness,Hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8750,7 +8750,7 @@ uint16_t mode_particlewaterfall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "Particle Waterfall@Particle Speed,Intensity,Speed Variation,Collision Hardness,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=150,ix=240,c1=0,c2=128,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) @@ -8853,12 +8853,11 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "Particle Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; /* -Perlin Noise 'gravity' mapping as in particles on 'noise hills' viewed from above -calculates slope gradient at the particle positions and applies 'downhill' force -restults in a fuzzy perlin noise display +Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above +calculates slope gradient at the particle positions and applies 'downhill' force, restulting in a fuzzy perlin noise display by DedeHai (Damian Schneider) */ @@ -8946,7 +8945,7 @@ uint16_t mode_particleperlin(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "Particle Perlin-Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; /* * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with @@ -8962,8 +8961,8 @@ uint16_t mode_particleimpact(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t Max_x(cols * PS_P_RADIUS - 1); + const uint32_t Max_y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 150; @@ -9095,8 +9094,8 @@ uint16_t mode_particleimpact(void) else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor - meteors[i].source.y = PS_MAX_Y + (PS_P_RADIUS << 2); // start 4 pixels above the top - meteors[i].source.x = random16(PS_MAX_X); + meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top + meteors[i].source.x = random16(Max_x); meteors[i].source.vy = -random(30) - 30; // meteor downward speed meteors[i].source.vx = random8(30) - 15; meteors[i].source.hue = random8(); // random color @@ -9115,7 +9114,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "Particle Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=35,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -9132,8 +9131,8 @@ uint16_t mode_particleattractor(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system box dimensions - const uint32_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const uint32_t Max_x(cols * PS_P_RADIUS - 1); + const uint32_t Max_y(rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 90; // maximum number of particles @@ -9165,8 +9164,8 @@ uint16_t mode_particleattractor(void) { attractor->vx = 0; attractor->vy = 0; - attractor->x = PS_MAX_X >> 1; // center - attractor->y = PS_MAX_Y >> 1; + attractor->x = Max_x >> 1; // center + attractor->y = Max_y >> 1; for (i = 0; i < numParticles; i++) { @@ -9247,7 +9246,7 @@ uint16_t mode_particleattractor(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "Particle Attractor@Center Mass,Particles,Emit Speed,Collisions,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; /* Particle Spray, just a simple spray animation with many parameters @@ -9264,8 +9263,8 @@ uint16_t mode_particlespray(void) const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle system x dimension - const uint32_t PS_MAX_X = (cols * PS_P_RADIUS - 1); - const uint32_t PS_MAX_Y = (rows * PS_P_RADIUS - 1); + const uint32_t Max_x = (cols * PS_P_RADIUS - 1); + const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 const uint32_t numParticles = 80; @@ -9321,8 +9320,8 @@ uint16_t mode_particlespray(void) { spray[i].source.hue++; // = random8(); //change hue of spray source // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PS_MAX_X); - spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PS_MAX_Y); + spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); + spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); } i = 0; @@ -9342,7 +9341,7 @@ uint16_t mode_particlespray(void) } } - uint8_t hardness = 200; + const uint8_t hardness = 200; if (SEGMENT.check3) // collisions enabled detectCollisions(particles, numParticles, hardness); @@ -9368,7 +9367,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "Particle Spray@Particle Speed,Intensity,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=100,ix=160,c1=100,c2=50,c3=20,o1=0,o2=1,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; /* Particle base Graphical Equalizer @@ -9485,7 +9484,7 @@ uint16_t mode_particleGEQ(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "Particle GEQ@Speed,Intensity,Randomness,Collision hardness,Gravity,Wrap X,Side bounce,Ground bounce;;!;012;pal=54,sx=100,ix=200,c1=0,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,WrapX,BounceX,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index ab3a911af1..1350eeb88b 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -176,12 +176,6 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bo // age part->ttl--; - // apply velocity - part->x += (int16_t)part->vx; - part->y += (int16_t)part->vy; - - part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - // apply velocity int32_t newX, newY; newX = part->x + (int16_t)part->vx; From e06a95f8479c2a80794787a879ed01c9f956c92e Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 21 Mar 2024 22:42:45 +0100 Subject: [PATCH 06/21] slight speed improvements in fire, like 1-2FPS --- wled00/FXparticleSystem.cpp | 81 ++++++++++++++++++++----------------- wled00/FXparticleSystem.h | 8 ++-- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1350eeb88b..3a8b864449 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -500,12 +500,14 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX void FireParticle_update(PSparticle *part, bool wrapX) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const int32_t PS_MAX_X=(cols * (uint32_t)PS_P_RADIUS - 1); + const int32_t PS_MAX_Y=(rows * (uint32_t)PS_P_RADIUS - 1); + + if (part->ttl > 0) { @@ -513,8 +515,8 @@ void FireParticle_update(PSparticle *part, bool wrapX) part->ttl--; // apply velocity - part->x = part->x + (int16_t)part->vx; - part->y = part->y + (int16_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + part->x = part->x + (int32_t)part->vx; + part->y = part->y + (int32_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire part->outofbounds = 0; // check if particle is out of bounds, wrap around to other side if wrapping is enabled @@ -550,11 +552,11 @@ void FireParticle_update(PSparticle *part, bool wrapX) void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) { - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); int32_t x, y; - uint8_t dx, dy; + uint32_t dx, dy; uint32_t tempVal; uint32_t i; @@ -562,37 +564,45 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0 || particles[i].outofbounds) + + if (particles[i].outofbounds) + { + continue; + } + + if (particles[i].ttl == 0) { continue; } + + dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); // compiler should optimize to bit shift - y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + x = (uint8_t)((uint16_t)particles[i].x >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + y = (uint8_t)((uint16_t)particles[i].y >> PS_P_RADIUS_SHIFT); if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { x--; // shift left - dx = dx + (PS_P_RADIUS >> 1); // add half a radius + dx += PS_P_RADIUS >> 1; // add half a radius } else // if jump has ocurred, fade pixel { // adjust dx so pixel fades - dx = dx - (PS_P_RADIUS >> 1); + dx -= PS_P_RADIUS >> 1; } if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached { y--; // shift row - dy = dy + (PS_P_RADIUS >> 1); + dy += PS_P_RADIUS >> 1; } else { // adjust dy so pixel fades - dy = dy - (PS_P_RADIUS >> 1); + dy -= PS_P_RADIUS >> 1; } if (wrapX) @@ -605,56 +615,55 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles // calculate brightness values for all six pixels representing a particle using linear interpolation // bottom left - if (x < cols && x >=0 && y < rows && y >=0) + //if (x < cols && x >=0 && y < rows && y >=0) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, rows); + PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // bottom right; x++; if (wrapX) { // wrap it to the other side if required - if (x >= cols) + //if (x >= cols) x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) } - if (x < cols && y < rows && y >= 0) + //if (x < cols && y < rows && y >= 0) { tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, rows); + PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // top right y++; - if (x < cols && y < rows) + //if (x < cols && y < rows) { tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, rows); + PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // top left x--; if (wrapX) { // wrap it to the other side if required - if (x < 0) - { // left half of particle render is out of frame, wrap it + if (x < 0) // left half of particle render is out of frame, wrap it x = cols - 1; - } + } - if (x < cols && x >= 0 && y < rows) + //if (x < cols && x >= 0 && y < rows) { tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal); - PartMatrix_addHeat(x + 1, y, tempVal); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + PartMatrix_addHeat(x, y, tempVal, rows); + PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } } } // adds 'heat' to red color channel, if it overflows, add it to next color channel -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) { - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) uint32_t newcolorvalue; @@ -665,9 +674,9 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat) heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire - uint8_t i = (colormode & 0x07) >> 1; + uint32_t i = (colormode & 0x07) >> 1; i = i % 3; - int8_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes + uint32_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes if (currentcolor[i] < 255) { newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 824acc7b37..6b01ee9c73 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,9 +30,11 @@ #include //particle dimensions (subpixel division) -#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement +#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS +#define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity -#define PS_P_SURFACE 12 //shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 + //todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! @@ -93,7 +95,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); void FireParticle_update(PSparticle *part, bool wrapX = false); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint16_t heat); +void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows); void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); void applyFriction(PSparticle *particle, uint8_t coefficient); From 65648140e286df027f835ab7b627a574e0370cbe Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 23 Mar 2024 20:11:23 +0100 Subject: [PATCH 07/21] Big update: lots of little fixes and big speed boost on fire animation -fixed fire burning more on the left side -fixed crash in particle attractor -added many improvements for ESP8266 -improved particle rendering efficiency -efficiency improvements in general -changed the way fire is rendered, now more than 2x faster -re-tuned fire to new rendering, also seperately tuned it for ESP8266 -changed all random8() to random16() as it runs faster on ESPs -some reformating -some renaming of effect stuff -fine tuning on falling particle effect -improvements to collision handling (faster and better) -added a (temporary) function for speed tests, will be removed again --- wled00/FX.cpp | 343 +++++++++++++--------- wled00/FXparticleSystem.cpp | 547 +++++++++++++++++------------------- wled00/FXparticleSystem.h | 15 +- 3 files changed, 470 insertions(+), 435 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 6bc85adaeb..3620ae51a0 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7898,7 +7898,7 @@ uint16_t mode_particlerotatingspray(void) const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles + const uint32_t numParticles = 170; // maximum number of particles #else const uint32_t numParticles = 700; // maximum number of particles #endif @@ -7956,7 +7956,7 @@ uint16_t mode_particlerotatingspray(void) { if (SEGMENT.check1) // random color is checked { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); } else { @@ -7967,13 +7967,18 @@ uint16_t mode_particlerotatingspray(void) } uint8_t percycle = spraycount; // maximum number of particles emitted per cycle - i = 0; - j = random(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + + #ifdef ESP8266 + if (SEGMENT.call & 0x01) //every other frame, do not emit to save particles + percycle = 0; +#endif + i = 0; + j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. while (i < numParticles) { if (particles[i].ttl == 0) // find a dead particle { - // spray[j].source.hue = random8(); //set random color for each particle (using palette) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % spraycount; if (percycle-- == 0) @@ -8030,10 +8035,8 @@ uint16_t mode_particlerotatingspray(void) } SEGMENT.fill(BLACK); // clear the matrix - // render the particles ParticleSys_render(particles, numParticles, false, false); - return FRAMETIME; } static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rotation Speed,Particle Speed,Arms,Flip Speed,Nozzle,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; @@ -8059,7 +8062,7 @@ uint16_t mode_particlefireworks(void) const uint32_t Max_y = (rows * PS_P_RADIUS - 1); #ifdef ESP8266 - const uint32_t numParticles = 100; + const uint32_t numParticles = 120; const uint8_t MaxNumRockets = 2; #else const uint32_t numParticles = 650; @@ -8094,14 +8097,14 @@ uint16_t mode_particlefireworks(void) } for (i = 0; i < numRockets; i++) { - rockets[i].source.ttl = random8(20 * i); // first rocket starts immediately, others follow soon + rockets[i].source.ttl = random16(20 * i); // first rocket starts immediately, others follow soon rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } // update particles, create particles - uint8_t circularexplosion = random8(numRockets + 2); // choose a rocket by random (but not every round one will be picked) - uint8_t spiralexplosion = random8(numRockets + 2); + uint8_t circularexplosion = random16(numRockets + 2); //choose a rocket by random (but not every round one will be picked) + uint8_t spiralexplosion = random16(numRockets + 2); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint16_t emitparticles; // number of particles to emit for each rocket's state @@ -8117,13 +8120,13 @@ uint16_t mode_particlefireworks(void) { // falling down emitparticles = 0; } - else - { // speed is zero, explode! + else // speed is zero, explode! + { #ifdef ESP8266 - emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else - emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion #endif rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) @@ -8137,8 +8140,8 @@ uint16_t mode_particlefireworks(void) uint8_t speed = 3; uint8_t angle = 0; - if (j == spiralexplosion) - angle = random(8); + if (j == spiralexplosion) + angle = random16(8); while(i < numParticles) { @@ -8153,10 +8156,10 @@ uint16_t mode_particlefireworks(void) { angle += 10; speed += 6; - rockets[j].source.hue = random8(); // new color for next row - rockets[j].source.sat = random8(); - if (emitparticles > 12) - emitparticles -= 12; // emitted about 12 particles for one circle, ensures no incomplete circles are done + rockets[j].source.hue = random16(); // new color for next row + rockets[j].source.sat = random16(); + if(emitparticles > 12) + emitparticles-=12; //emitted about 12 particles for one circle, ensures no incomplete circles are done else emitparticles = 0; } @@ -8170,8 +8173,9 @@ uint16_t mode_particlefireworks(void) emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise angle += 15; speed++; - rockets[j].source.hue++; - rockets[j].source.sat = random8(155) + 100; + rockets[j].source.hue++; + rockets[j].source.sat = random16(155)+100; + } else if (emitparticles > 0) { @@ -8204,25 +8208,25 @@ uint16_t mode_particlefireworks(void) else if (rockets[i].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[i].source.hue = random8(); // random color - rockets[i].source.sat = random8(100) + 155; + rockets[i].source.hue = random16(); // random color + rockets[i].source.sat = random16(100)+155; rockets[i].maxLife = 200; rockets[i].minLife = 50; - rockets[i].source.ttl = random8((255 - SEGMENT.speed)) + 50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds - rockets[i].vx = 0; // emitting speed - rockets[i].vy = 0; // emitting speed - rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + rockets[i].source.ttl = random16((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds + rockets[i].vx = 0; // emitting speed + rockets[i].vy = 0; // emitting speed + rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) } else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket rockets[i].source.y = 1; // start from bottom rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[i].source.vy = random8(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[i].source.vx = random8(5) - 2; + rockets[i].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height + rockets[i].source.vx = random16(5) - 2; rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) rockets[i].source.sat = 30; // low saturation -> exhaust is off-white - rockets[i].source.ttl = random8(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[i].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) rockets[i].maxLife = 30; // exhaust particle life rockets[i].minLife = 10; rockets[i].vx = 0; // emitting speed @@ -8237,7 +8241,7 @@ uint16_t mode_particlefireworks(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Wrap X,Bounce X,Bounce Y;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; /* * Particle Volcano (gravity spray) @@ -8290,17 +8294,17 @@ uint16_t mode_particlevolcano(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. spray[i].source.vx = 0; spray[i].maxLife = 300; // lifetime in frames spray[i].minLife = 20; - spray[i].source.collide = true; // seeded particles will collide - spray[i].vx = 0; // emitting speed - spray[i].vy = 20; // emitting speed - // spray.var = 10 + (random8() % 4); + spray[i].source.collide = true; //seeded particles will collide + spray[i].vx = 0; // emitting speed + spray[i].vy = 20; // emitting speed + // spray.var = 10 + (random16() % 4); } } @@ -8309,7 +8313,7 @@ uint16_t mode_particlevolcano(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source + spray[i].source.hue++; // = random16(); //change hue of spray source // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing if (SEGMENT.check2) // bounce { @@ -8339,8 +8343,8 @@ uint16_t mode_particlevolcano(void) for (i = 0; i < numParticles; i++) { if (particles[i].ttl == 0) // find a dead particle - { - // spray[j].source.hue = random8(); //set random color for each particle (using palette) + { + // spray[j].source.hue = random16(); //set random color for each particle (using palette) Emitter_Fountain_emit(&spray[j], &particles[i]); j = (j + 1) % numSprays; if (percycle-- == 0) @@ -8374,7 +8378,58 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Bounce X,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; + +//for debugging speed tests, this speed test function can be used, compiler will not optimize it +void __attribute__((optimize("O0"))) SpeedTestfunction(void) +{ + // unmodifiable compiler code + Serial.print("Speedtest: "); + int32_t i; + volatile int32_t randomnumber; + uint32_t start = micros(); + uint32_t time; + volatile int32_t windspeed; + for (i = 0; i < 100000; i++) + { + //windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + randomnumber = random8(); + } + time = micros() - start; + Serial.print(time); + Serial.print(" "); + start = micros(); + for (i = 0; i < 100000; i++) + { + //windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + randomnumber = random8(15); + } + time = micros() - start; + Serial.print(time); + Serial.print(" "); + + start = micros(); + for (i = 0; i < 100000; i++) + { + // windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + randomnumber = random16(); + } + time = micros() - start; + Serial.print(time); + Serial.print(" "); + start = micros(); + for (i = 0; i < 100000; i++) + { + // windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + randomnumber = random16(15); + } + + time = micros() - start; + Serial.print(time); + Serial.print(" "); + + Serial.println(" ***"); +} /* * Particle Fire @@ -8384,6 +8439,10 @@ static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Int uint16_t mode_particlefire(void) { + + // speed tests : + // SpeedTestfunction(); + if (SEGLEN == 1) return mode_static(); @@ -8391,18 +8450,20 @@ uint16_t mode_particlefire(void) // particle system box dimensions const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - + #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)10, cols); // limit to 10 flames, not enough ram on ESP8266 - const uint32_t numParticles = numFlames * 16; - const uint32_t numNormalFlames = numFlames - (cols >> 2); // number of normal flames, rest of flames are baseflames + const uint32_t numFlames = min((uint32_t)12, (cols<<1)); // limit to 18 flames, not enough ram on ESP8266 + const uint32_t numParticles = numFlames * 15; //limit number of particles to about 180 or ram will be depleted + const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames + uint32_t percycle = numFlames >> 2 ;// maximum number of particles emitted per cycle #else const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 20; - const uint32_t numNormalFlames = numFlames - (cols/3); // number of normal flames, rest of flames are baseflames - #endif + const uint32_t numParticles = numFlames * 18; + const uint32_t numNormalFlames = numFlames - (cols / 3); // number of normal flames, rest of flames are baseflames + uint32_t percycle = numFlames / 3; // maximum number of particles emitted per cycle +#endif + - uint8_t percycle = numFlames >> 1; // maximum number of particles emitted per cycle PSparticle *particles; PSpointsource *flames; @@ -8434,9 +8495,8 @@ uint16_t mode_particlefire(void) for (i = 0; i < numFlames; i++) { flames[i].source.ttl = 0; - flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners flames[i].source.vx = 0; // emitter moving speed; - flames[i].source.vy = 0; + flames[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) } } @@ -8446,75 +8506,80 @@ uint16_t mode_particlefire(void) { if (flames[i].source.ttl > 0) { - flames[i].source.ttl--; - flames[i].source.x += flames[i].source.vx; // move the flame source (if it has x-speed) + flames[i].source.ttl--; } else // flame source is dead { - // initialize new flame: set properties of source - // from time to time, chang the flame position - // make some of the flames small and slow to add a bright base - - if (random8(40) == 0) // from time to time, change flame position (about once per second at 40 fps) + // initialize new flame: set properties of source + if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { if (SEGMENT.check1) { // wrap around in X direction, distribute randomly - flames[i].source.x = random16(Max_x); + flames[i].source.x = rand() % Max_x; } else // no X-wrapping - { - flames[i].source.x = PS_P_RADIUS * 3 + random16(Max_x - (PS_P_RADIUS * 6)); // distribute randomly but not close to the corners + { + flames[i].source.x = (rand() % (Max_x - (PS_P_RADIUS * ((cols>>3)+1)))) + PS_P_RADIUS * ((cols>>4)+1); // distribute randomly but not close to the corners (cannot use random16() it is not random enough, tends to burn on left side) } } flames[i].source.y = -PS_P_RADIUS; // set the source below the frame - if (i < numNormalFlames) - { // all but the last few are normal flames - flames[i].source.ttl = random8(SEGMENT.intensity >> 2) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - flames[i].maxLife = random8(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 3; - flames[i].vx = (int8_t)random8(4) - 2; // emitting speed (sideways) + if (i < numNormalFlames) + { + flames[i].source.ttl = random16((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + flames[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + flames[i].minLife = 4; + flames[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) - flames[i].var = random8(5) + 3; // speed variation around vx,vy (+/- var/2) + flames[i].var = random16(5) + 3;; // speed variation around vx,vy (+/- var/2) } else - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.ttl = random8(25) + 15; // lifetime of one flame + { // base flames to make the base brighter, flames are slower and short lived + flames[i].source.ttl = random16(25) + 15; // lifetime of one flame flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height flames[i].minLife = 12; flames[i].vx = 0; // emitting speed, sideways - flames[i].vy = 1 + (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - flames[i].var = 2; // speed variation around vx,vy (+/- var/2) + flames[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) + flames[i].var = 5; // speed variation around vx,vy (+/- var/2) } } } - SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call & 0x01) //update noise position every second frames + { + SEGMENT.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call & 0x02) //every tird frame + SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often + } + int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0,SEGMENT.aux1) - 127) / ((271 - SEGMENT.custom2) >> 4); // update particles, create particles uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) for (i = 0; i < numParticles; i++) { - if (particles[i].ttl == 0 && percycle > 0) + if (particles[i].ttl == 0) { - Emitter_Flame_emit(&flames[j], &particles[i]); - j++; - if (j >= numFlames) - { // or simpler: j=j%numFlames; - j = 0; + if(percycle > 0) + { + Emitter_Flame_emit(&flames[j], &particles[i]); + j++; + if (j >= numFlames) + { // or simpler: j=j%numFlames; but that is slow on ESP8266 + j = 0; + } + percycle--; } - percycle--; } - else if (particles[i].ttl) + else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 { - // add wind using perlin noise - int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0, particles[i].y >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); + // add wind using perlin noise particles[i].vx = windspeed; - FireParticle_update(&particles[i], SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user } } + FireParticle_update(particles, numParticles, SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user + SEGMENT.fill(BLACK); // clear the matrix // render the particles @@ -8522,10 +8587,10 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, WrapX;;!;012;sx=100,ix=120,c1=30,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, Cylinder;;!;012;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; /* -PS Hail: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce +PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color @@ -8576,18 +8641,18 @@ uint16_t mode_particlefall(void) // emit particle at random position just over the top of the matrix particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - if (random8(5) == 0) // 16% of particles apper anywhere + if (random16(5) == 0) // 16% of particles apper anywhere particles[i].x = random16(cols * PS_P_RADIUS - 1); else // rest is emitted at center half particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height - particles[i].vx = (((int16_t)random8(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider + particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider particles[i].vy = -(SEGMENT.speed >> 1); - particles[i].hue = random8(); // set random color + particles[i].hue = random16(); // set random color particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - particles[i].collide = true; // particle will collide - break; // quit loop if all particles of this round emitted + particles[i].collide = true; // particle will collide + break; //emit only one particle per round } i++; } @@ -8597,14 +8662,18 @@ uint16_t mode_particlefall(void) detectCollisions(particles, numParticles, hardness); // now move the particles + + uint32_t frictioncoefficient = 1; + if (SEGMENT.speed < 50) + { + frictioncoefficient = 50 - SEGMENT.speed; + } + for (i = 0; i < numParticles; i++) { - // apply 'air friction' to smooth things out, slows down all particles depending on their speed, only done on low speeds - if (SEGMENT.speed < 50) - { - applyFriction(&particles[i], 50 - SEGMENT.speed); - } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness, (uint8_t)150)); // surface hardness max is 150 + // apply 'air friction' to smooth things out, slows down all particles depending on their speed + applyFriction(&particles[i], frictioncoefficient); + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)200)); // surface hardness max is 200 } SEGMENT.fill(BLACK); // clear the matrix @@ -8614,7 +8683,7 @@ uint16_t mode_particlefall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Hail@Speed,Intensity,Randomness,Hardness,Saturation,Wrap X,Side bounce,Ground bounce;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8665,7 +8734,7 @@ uint16_t mode_particlewaterfall(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2 * PS_P_RADIUS * (i); spray[i].source.y = (rows + 4) * (PS_P_RADIUS * (i + 1)); // source y position, few pixels above the top to increase spreading before entering the matrix @@ -8750,7 +8819,7 @@ uint16_t mode_particlewaterfall(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Wrap X,Bounce X,Ground bounce;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) @@ -8807,8 +8876,8 @@ uint16_t mode_particlebox(void) int32_t ygravity; SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); + + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); if (SEGMENT.check1) // sloshing, y force is alwys downwards { @@ -8998,7 +9067,7 @@ uint16_t mode_particleimpact(void) for (i = 0; i < MaxNumMeteors; i++) { meteors[i].source.y = 10; - meteors[i].source.ttl = random8(20 * i); // set initial delay for meteors + meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched meteors[i].source.sat = 255; // full saturation, color chosen by palette } @@ -9027,9 +9096,9 @@ uint16_t mode_particleimpact(void) { meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 - emitparticles = random8(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion #else - emitparticles = random8(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion #endif } @@ -9082,9 +9151,9 @@ uint16_t mode_particleimpact(void) meteors[i].maxLife = 200; meteors[i].minLife = 50; #ifdef ESP8266 - meteors[i].source.ttl = random8(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - meteors[i].source.ttl = random8((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif meteors[i].vx = 0; // emitting speed x meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y @@ -9096,10 +9165,10 @@ uint16_t mode_particleimpact(void) // reinitialize meteor meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top meteors[i].source.x = random16(Max_x); - meteors[i].source.vy = -random(30) - 30; // meteor downward speed - meteors[i].source.vx = random8(30) - 15; - meteors[i].source.hue = random8(); // random color - meteors[i].source.ttl = 1000; // long life, will explode at bottom + meteors[i].source.vy = -random16(30) - 30; // meteor downward speed + meteors[i].source.vx = random16(30) - 15; + meteors[i].source.hue = random16(); // random color + meteors[i].source.ttl = 1000; // long life, will explode at bottom meteors[i].source.collide = false; // trail particles will not collide meteors[i].maxLife = 60; // spark particle life meteors[i].minLife = 20; @@ -9114,7 +9183,7 @@ uint16_t mode_particleimpact(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Wrap X,Bounce X,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -9172,12 +9241,12 @@ uint16_t mode_particleattractor(void) particles[i].ttl = 0; } - spray->source.hue = random8(); - spray->source.sat = 255; // full saturation, color by palette + spray->source.hue = random16(); + spray->source.sat = 255; //full saturation, color by palette spray->source.x = 0; - spray->source.y = 0; - spray->source.vx = random8(5) + 2; - spray->source.vy = random8(4) + 1; + spray->source.y = 0; + spray->source.vx = random16(5) + 2; + spray->source.vy = random16(4) + 1; spray->source.ttl = 100; spray->source.collide = true; // seeded particles will collide (if checked) spray->maxLife = 300; // seeded particle lifetime in frames @@ -9187,7 +9256,7 @@ uint16_t mode_particleattractor(void) spray->var = 6; // emitting speed variation } - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 1; //TODO: the -1 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 2; //TODO: the -2 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. i = 0; @@ -9299,7 +9368,7 @@ uint16_t mode_particlespray(void) } for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random8(); + spray[i].source.hue = random16(); spray[i].source.sat = 255; // set full saturation spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. @@ -9318,7 +9387,7 @@ uint16_t mode_particlespray(void) { for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random8(); //change hue of spray source + spray[i].source.hue++; // = random16(); //change hue of spray source // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); @@ -9330,7 +9399,7 @@ uint16_t mode_particlespray(void) { if (particles[i].ttl == 0) // find a dead particle { - // spray[j].source.hue = random8(); //set random color for each particle (using palette) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); j = (j + 1) % numSprays; if (percycle-- == 0) @@ -9367,7 +9436,7 @@ uint16_t mode_particlespray(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,X Position,Y Position,Angle,Gravity,WrapX/Bounce,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; /* Particle base Graphical Equalizer @@ -9440,8 +9509,8 @@ uint16_t mode_particleGEQ(void) } else if (fftResult[bin] > 0) // band has low volue { - uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; - if (random8() % restvolume == 0) + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (random16() % restvolume == 0) { emitparticles = 1; } @@ -9450,31 +9519,29 @@ uint16_t mode_particleGEQ(void) while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { if (particles[i].ttl == 0) // find a dead particle - { - // set particle properties - particles[i].ttl = map(SEGMENT.intensity, 0, 255, emitspeed >> 1, emitspeed + random8(emitspeed)); // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random8(binwidth) - (binwidth >> 1); // position randomly, deviating half a bin width - particles[i].y = 0; // start at the bottom - particles[i].vx = random8(SEGMENT.custom1 >> 1) - (SEGMENT.custom1 >> 2); // x-speed variation + { + //set particle properties + particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width + particles[i].y = 0; //start at the bottom + particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation particles[i].vy = emitspeed; - particles[i].hue = (bin << 4) + random8(17) - 8; // color from palette according to bin - // particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation emitparticles--; } i++; } } - // Serial.println(" "); - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. // detectCollisions(particles, numParticles, hardness); // now move the particles for (i = 0; i < numParticles; i++) - { - particles[i].vy -= (SEGMENT.custom3 >> 3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); + { + particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity + Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); } SEGMENT.fill(BLACK); // clear the matrix diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a8b864449..eb1fa972e6 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -40,22 +40,22 @@ // Fountain style emitter for particles used for flames (particle TTL depends on source TTL) void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x + random8(emitter->var) - (emitter->var >> 1); - part->y = emitter->source.y + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->x = emitter->source.x + random16(PS_P_RADIUS) - PS_P_HALFRADIUS; // jitter the flame by one pixel to make the flames wider and softer + part->y = emitter->source.y; + part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL - part->hue = emitter->source.hue; - //part->sat = emitter->source.sat; //flame does not use saturation + // part->hue = emitter->source.hue; //fire uses ttl and not hue for heat + // part->sat = emitter->source.sat; //flame does not use saturation } // fountain style emitter void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) { - part->x = emitter->source.x; // + random8(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. - part->y = emitter->source.y; // + random8(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random8(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random8(emitter->var) - (emitter->var >> 1); + part->x = emitter->source.x; // + random16(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. + part->y = emitter->source.y; // + random16(emitter->var) - (emitter->var >> 1); + part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); + part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; part->hue = emitter->source.hue; part->sat = emitter->source.sat; @@ -65,16 +65,16 @@ void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) // Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed) { - emitter->vx = (((int16_t)cos8(angle)-127) * speed) >> 7; //cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 - emitter->vy = (((int16_t)sin8(angle)-127) * speed) >> 7; + emitter->vx = (((int16_t)cos8(angle) - 127) * speed) >> 7; // cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 + emitter->vy = (((int16_t)sin8(angle) - 127) * speed) >> 7; Emitter_Fountain_emit(emitter, part); } // attracts a particle to an attractor particle using the inverse square-law void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) { // Calculate the distance between the particle and the attractor - int dx = attractor->x - particle->x; - int dy = attractor->y - particle->y; + int32_t dx = attractor->x - particle->x; + int32_t dy = attractor->y - particle->y; // Calculate the force based on inverse square law int32_t distanceSquared = dx * dx + dy * dy + 1; @@ -87,7 +87,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co } distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces } - + int32_t shiftedstrength = (int32_t)strength << 16; int32_t force; int32_t xforce; @@ -124,7 +124,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vx += 1; } } - else //save counter value + else // save counter value *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits } else @@ -150,7 +150,7 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co particle->vy += 1; } } - else // save counter value + else // save counter value *counter |= (ycounter << 4) & 0xF0; // write upper four bits } else @@ -164,60 +164,59 @@ void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *co void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; + const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; if (part->ttl > 0) { - // age - part->ttl--; - - // apply velocity - int32_t newX, newY; - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; + // age + part->ttl--; - part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - // x direction, handle wraparound - if (wrapX) - { - newX = newX % (PS_MAX_X + 1); - if (newX < 0) - newX = PS_MAX_X - newX; - } - else if ((part->x <= 0) || (part->x >= PS_MAX_X)) // check if particle is out of bounds - { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->x = newX; // set new position + // apply velocity + int32_t newX, newY; + newX = part->x + (int16_t)part->vx; + newY = part->y + (int16_t)part->vy; - if (wrapY) - { - newY = newY % (PS_MAX_Y + 1); - if (newY < 0) - newY = PS_MAX_Y - newY; - } - else if ((part->y <= 0) || (part->y >= PS_MAX_Y)) // check if particle is out of bounds - { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->y = newY; // set new position - } + part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + // x direction, handle wraparound + if (wrapX) + { + newX = newX % (PS_MAX_X + 1); + if (newX < 0) + newX = PS_MAX_X - newX; + } + else if ((part->x <= 0) || (part->x >= PS_MAX_X)) // check if particle is out of bounds + { + if (killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; + } + part->x = newX; // set new position + if (wrapY) + { + newY = newY % (PS_MAX_Y + 1); + if (newY < 0) + newY = PS_MAX_Y - newY; + } + else if ((part->y <= 0) || (part->y >= PS_MAX_Y)) // check if particle is out of bounds + { + if (killoutofbounds) + part->ttl = 0; + else + part->outofbounds = 1; + } + part->y = newY; // set new position + } } // bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) +void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) { // Matrix dimension const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; @@ -242,15 +241,15 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) newY = part->y + (int16_t)part->vy; if ((newX <= 0) || (newX >= PS_MAX_X)) - { // reached an edge - part->vx = -part->vx; // invert speed - part->vx = (((int16_t)part->vx) * ((int16_t)hardness+1)) >> 8; // reduce speed as energy is lost on non-hard surface + { // reached an edge + part->vx = -part->vx; // invert speed + part->vx = (((int16_t)part->vx) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface } if ((newY <= 0) || (newY >= PS_MAX_Y)) - { // reached an edge - part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * ((int16_t)hardness+1)) >> 8; // reduce speed as energy is lost on non-hard surface + { // reached an edge + part->vy = -part->vy; // invert speed + part->vy = (((int16_t)part->vy) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface } newX = max(newX, (int16_t)0); // limit to positive @@ -258,19 +257,18 @@ void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries part->y = min(newY, (int16_t)PS_MAX_Y); } - } // particle moves, gravity force is applied and ages, if wrapX is set, pixels leaving in x direction reappear on other side, hardness is surface hardness for bouncing (127 means 50% speed lost each bounce) -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) +void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) { // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; + const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; if (part->ttl > 0) { @@ -300,7 +298,7 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo } // apply velocity - int16_t newX, newY; + int32_t newX, newY; newX = part->x + (int16_t)part->vx; newY = part->y + (int16_t)part->vy; @@ -323,8 +321,8 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo { part->vx = -part->vx; // invert speed part->vx = (((int16_t)part->vx) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newX = max(newX, (int16_t)0); // limit to positive - newX = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries + newX = max(newX, (int32_t)0); // limit to positive + newX = min(newX, (int32_t)PS_MAX_X); // limit to matrix boundaries } else // not bouncing and out of matrix part->outofbounds = 1; @@ -339,9 +337,9 @@ void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bo if (bounceY) { part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newY = max(newY, (int16_t)0); // limit to positive (helps with piling as that can push particles out of frame) - // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface + newY = max(newY, (int32_t)0); // limit to positive (helps with piling as that can push particles out of frame) + // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries } else // not bouncing and out of matrix part->outofbounds = 1; @@ -360,21 +358,19 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX #else bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add #endif - - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - int16_t x, y; - uint8_t dx, dy; + int32_t x, y; + uint32_t dx, dy; uint32_t intensity; CRGB baseRGB; uint32_t i; - uint8_t brightess; // particle brightness, fades if dying - + uint32_t brightess; // particle brightness, fades if dying + + uint32_t precal1, precal2, precal3; // precalculate values to improve speed + // go over particles and update matrix cells on the way for (i = 0; i < numParticles; i++) @@ -395,37 +391,19 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX else baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); - dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - - x = (uint8_t)((uint16_t)particles[i].x / (uint16_t)PS_P_RADIUS); - y = (uint8_t)((uint16_t)particles[i].y / (uint16_t)PS_P_RADIUS); + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; + dx = xoffset % (uint32_t)PS_P_RADIUS; + dy = yoffset % (uint32_t)PS_P_RADIUS; + x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + y = (yoffset) >> PS_P_RADIUS_SHIFT; - // for vx=1, vy=1: starts out with all four pixels at the same color (32/32) - // moves to upper right pixel (64/64) - // then moves one physical pixel up and right(+1/+1), starts out now with - // lower left pixel fully bright (0/0) and moves to all four pixel at same - // color (32/32) - - if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - x--; // shift x to next pixel left, will overflow to 255 if 0 - dx = dx + (PS_P_RADIUS >> 1); - } - else // if jump has ocurred - { - dx = dx - (PS_P_RADIUS >> 1); // adjust dx so pixel fades - } - - if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - y--; // shift y to next pixel down, will overflow to 255 if 0 - dy = dy + (PS_P_RADIUS >> 1); - } - else - { - dy = dy - (PS_P_RADIUS >> 1); - } + // calculate brightness values for all six pixels representing a particle using linear interpolation + // precalculate values for speed optimization + precal1 = PS_P_RADIUS - dx; + precal2 = (PS_P_RADIUS - dy) * brightess; // multiply by ttl, adds more heat for younger particles + precal3 = dy * brightess; if (wrapX) { // wrap it to the other side if required @@ -447,13 +425,11 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX // intensity is a scaling value from 0-255 (0-100%) // bottom left - if (x < cols && y < rows) - { - // calculate the intensity with linear interpolation - intensity = ((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy - // scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } + // calculate the intensity with linear interpolation, divide by surface area (shift by PS_P_SURFACE) to distribute the energy + intensity = (precal1 * precal2) >> PS_P_SURFACE; // equal to (PS_P_RADIUS - dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + // scale the particle base color by the intensity and add it to the pixel + SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); + // bottom right; x++; if (wrapX) @@ -463,7 +439,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + intensity = (dx * precal2) >> PS_P_SURFACE; // equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } // top right @@ -475,7 +451,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)dx * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + intensity = (dx * precal3) >> PS_P_SURFACE; // equal to (dx * dy * brightess) >> PS_P_SURFACE SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } // top left @@ -489,7 +465,7 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX } if (x < cols && y < rows) { - intensity = ((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)brightess) >> PS_P_SURFACE; // divide by PS_P_SURFACE to distribute the energy + intensity = (precal1 * precal3) >> PS_P_SURFACE; // equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); } } @@ -497,52 +473,56 @@ void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX // update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true // particles move upwards faster if ttl is high (i.e. they are hotter) -void FireParticle_update(PSparticle *part, bool wrapX) +void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX) { // Matrix dimension const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); // particle box dimensions - const int32_t PS_MAX_X=(cols * (uint32_t)PS_P_RADIUS - 1); - const int32_t PS_MAX_Y=(rows * (uint32_t)PS_P_RADIUS - 1); - + const int32_t PS_MAX_X = (cols * (uint32_t)PS_P_RADIUS - 1); + const int32_t PS_MAX_Y = (rows * (uint32_t)PS_P_RADIUS - 1); + uint32_t i = 0; - - if (part->ttl > 0) + for (i = 0; i < numparticles; i++) { - // age - part->ttl--; - - // apply velocity - part->x = part->x + (int32_t)part->vx; - part->y = part->y + (int32_t)part->vy + (part->ttl >> 4); // younger particles move faster upward as they are hotter, used for fire - - part->outofbounds = 0; - // check if particle is out of bounds, wrap around to other side if wrapping is enabled - // x-direction - if ((part->x < 0) || (part->x > PS_MAX_X)) + if (part[i].ttl > 0) { - if (wrapX) + // age + part[i].ttl--; + + // apply velocity + part[i].x = part[i].x + (int32_t)part[i].vx; + part[i].y = part[i].y + (int32_t)part[i].vy + (part[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire + + part[i].outofbounds = 0; + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve animation speed, only check x direction if y is not out of bounds + // y-direction + if (part[i].y < 0) { - part->x = part->x % (PS_MAX_X + 1); - if (part->x < 0) - part->x = PS_MAX_X - part->x; + part[i].outofbounds = 1; } - else + else if (part[i].y > PS_MAX_Y) // particle moved out on the top { - part->ttl = 0; + part[i].ttl = 0; + } + else // particle is in frame in y direction, also check x direction now + { + if ((part[i].x < 0) || (part[i].x > PS_MAX_X)) + { + if (wrapX) + { + part[i].x = part[i].x % (PS_MAX_X + 1); + if (part[i].x < 0) + part[i].x = PS_MAX_X - part[i].x; + } + else + { + part[i].ttl = 0; + } + } } - } - - // y-direction - if ((part->y < -(PS_P_RADIUS << 4)) || (part->y > PS_MAX_Y)) - { // position up to 8 pixels below the matrix is allowed, used for wider flames at the bottom - part->ttl = 0; - } - else if (part->y < 0) - { - part->outofbounds = 1; } } } @@ -552,20 +532,20 @@ void FireParticle_update(PSparticle *part, bool wrapX) void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) { - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); int32_t x, y; uint32_t dx, dy; - uint32_t tempVal; + uint32_t pixelheat; + uint32_t precal1, precal2, precal3; // precalculated values to improve speed uint32_t i; - // go over particles and update matrix cells on the way // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) for (i = 0; i < numParticles; i++) { - if (particles[i].outofbounds) + if (particles[i].outofbounds) { continue; } @@ -575,38 +555,16 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles continue; } - - - dx = (uint8_t)((uint16_t)particles[i].x % (uint16_t)PS_P_RADIUS); - dy = (uint8_t)((uint16_t)particles[i].y % (uint16_t)PS_P_RADIUS); - - x = (uint8_t)((uint16_t)particles[i].x >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (uint8_t)((uint16_t)particles[i].y >> PS_P_RADIUS_SHIFT); - - if (dx < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - x--; // shift left - dx += PS_P_RADIUS >> 1; // add half a radius - } - else // if jump has ocurred, fade pixel - { - // adjust dx so pixel fades - dx -= PS_P_RADIUS >> 1; - } + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; + dx = xoffset % (uint32_t)PS_P_RADIUS; + dy = yoffset % (uint32_t)PS_P_RADIUS; + x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + y = (yoffset) >> PS_P_RADIUS_SHIFT; - if (dy < (PS_P_RADIUS >> 1)) // jump to next physical pixel if half of virtual pixel size is reached - { - y--; // shift row - dy += PS_P_RADIUS >> 1; - } - else + if (wrapX) { - // adjust dy so pixel fades - dy -= PS_P_RADIUS >> 1; - } - - if (wrapX) - { if (x < 0) { // left half of particle render is out of frame, wrap it x = cols - 1; @@ -614,47 +572,51 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles } // calculate brightness values for all six pixels representing a particle using linear interpolation + // precalculate values for speed optimization + precal1 = PS_P_RADIUS - dx; + precal2 = (PS_P_RADIUS - dy) * particles[i].ttl; //multiply by ttl, adds more heat for younger particles + precal3 = dy * particles[i].ttl; + // bottom left - //if (x < cols && x >=0 && y < rows && y >=0) + if (x < cols && x >= 0 && y < rows && y >= 0) { - tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, rows); - PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + pixelheat = (precal1 * precal2) >> PS_P_SURFACE; + PartMatrix_addHeat(x, y, pixelheat, rows); + // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // bottom right; x++; if (wrapX) - { // wrap it to the other side if required - //if (x >= cols) + { // wrap it to the other side if required + if (x >= cols) // if statement is faster on ESP8266 TODO: add a define x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) } - //if (x < cols && y < rows && y >= 0) + if (x < cols && y < rows && y >= 0) { - tempVal = (((uint32_t)dx * ((PS_P_RADIUS)-dy) * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, rows); - PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + pixelheat = (dx * precal2) >> PS_P_SURFACE; + PartMatrix_addHeat(x, y, pixelheat, rows); + // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // top right y++; - //if (x < cols && y < rows) + if (x < cols && y < rows) { - tempVal = (((uint32_t)dx * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); // - PartMatrix_addHeat(x, y, tempVal, rows); - PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + pixelheat = (dx * precal3) >> PS_P_SURFACE; // + PartMatrix_addHeat(x, y, pixelheat, rows); + // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } // top left x--; if (wrapX) - { // wrap it to the other side if required + { // wrap it to the other side if required if (x < 0) // left half of particle render is out of frame, wrap it x = cols - 1; - } - //if (x < cols && x >= 0 && y < rows) + if (x < cols && x >= 0 && y < rows) { - tempVal = (((uint32_t)((PS_P_RADIUS)-dx) * dy * (uint32_t)particles[i].ttl) >> PS_P_SURFACE); - PartMatrix_addHeat(x, y, tempVal, rows); - PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + pixelheat = (precal1 * precal3) >> PS_P_SURFACE; + PartMatrix_addHeat(x, y, pixelheat, rows); + // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) } } } @@ -663,16 +625,19 @@ void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) { - //const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + // const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) uint32_t newcolorvalue; - uint8_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) + uint32_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) // define how the particle TTL value (which is the heat given to the function) maps to heat, if lower, fire is more red, if higher, fire is brighter as bright flames travel higher and decay faster - - heat = heat << 3; // need to take a larger value to scale ttl value of particle to a good heat value that decays fast enough - + // need to scale ttl value of particle to a good heat value that decays fast enough + #ifdef ESP8266 + heat = heat << 4; //ESP8266 has no hardware multiplication, just use shift (also less particles, need more heat) + #else + heat = heat * 10; + #endif // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire uint32_t i = (colormode & 0x07) >> 1; i = i % 3; @@ -685,7 +650,7 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) if (newcolorvalue == 255) { // there cannot be a leftover if it is not full heat = heat - (255 - currentcolor[i]); // heat added is difference from current value to full value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value + // this cannot produce an underflow since we never add more than the initial heat value } else { @@ -701,13 +666,13 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) if (currentcolor[i] < 255) { - newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows + newcolorvalue = (uint32_t)currentcolor[i] + heat; // add heat, check if it overflows newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again // check if there is heat left over if (newcolorvalue == 255) // there cannot be a leftover if red is not full { heat = heat - (255 - currentcolor[i]); // heat added is difference from current red value to full red value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value + // this cannot produce an underflow since we never add more than the initial heat value } else { @@ -732,23 +697,23 @@ void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) } // detect collisions in an array of particles and handle them -void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hardness) +void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness) { // detect and handle collisions - uint32_t i,j; - int32_t startparticle = 0; - int32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up if (SEGMENT.call % 2 == 0) { // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) startparticle = endparticle; endparticle = numparticles; } - + for (i = startparticle; i < endparticle; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].collide && particles[i].outofbounds==0) // if particle is alive and does collide and is not out of view + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].collide && particles[i].outofbounds == 0) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles for (j = i + 1; j < numparticles; j++) @@ -756,7 +721,7 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; - if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS))) //check x direction, if close, check y direction + if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS))) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS)) && (dy < (PS_P_HARDRADIUS)) && (dy > (-PS_P_HARDRADIUS))) @@ -771,37 +736,44 @@ void detectCollisions(PSparticle* particles, uint32_t numparticles, uint8_t hard } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS -// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision) -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness) +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness) { int32_t dx = particle2->x - particle1->x; int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) { - // Adjust positions based on relative velocity direction - - if (relativeVx < 0) { //if true, particle2 is on the right side - particle1->x--; - particle2->x++; - } else{ - particle1->x++; - particle2->x--; - } - - if (relativeVy < 0) { - particle1->y--; - particle2->y++; - } else{ - particle1->y++; - particle2->y--; - } + // Adjust positions based on relative velocity direction TODO: is this really needed? only happens on fast particles, would save some code (but make it a tiny bit less accurate on fast particles but probably not an issue) + + if (relativeVx < 0) + { // if true, particle2 is on the right side + particle1->x--; + particle2->x++; + } + else + { + particle1->x++; + particle2->x--; + } + + if (relativeVy < 0) + { + particle1->y--; + particle2->y++; + } + else + { + particle1->y++; + particle2->y--; + } distanceSquared++; } // Calculate dot product of relative velocity and relative distance @@ -810,77 +782,82 @@ void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t // If particles are moving towards each other if (dotProduct < 0) { - const uint8_t bitshift = 14; // bitshift used to avoid floats + const uint32_t bitshift = 14; // bitshift used to avoid floats // Calculate new velocities after collision - int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * hardness) >> 8; + int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * (hardness+1)) >> 8; int32_t ximpulse = (impulse * dx) >> bitshift; int32_t yimpulse = (impulse * dy) >> bitshift; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - - if (hardness < 50) // if particles are soft, they become 'sticky' i.e. slow movements are stopped + /* + //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky + //also second version using multiplication is slower on ESP8266 than the if's + if (hardness < 50) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions { - particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; - particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; + //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + + //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; + //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; - particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; - } - } + const uint32_t coeff = 100; + particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; + particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; + + particle2->vx = ((int32_t)particle2->vx * coeff) >> 8; + particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; + }*/ + } // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter - if (distanceSquared < (int32_t)2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) + if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) { - uint8_t choice = random8(2);//randomly choose one particle to push, avoids oscillations - const int32_t HARDDIAMETER = (int32_t)2*PS_P_HARDRADIUS; - + uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number + const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; + const int32_t pushamount = 2; //push a small amount + int32_t push = pushamount; if (dx < HARDDIAMETER && dx > -HARDDIAMETER) { // distance is too small, push them apart - int32_t push; if (dx <= 0) - push = -1;//-(PS_P_HARDRADIUS + dx); // inverted push direction - else - push = 1;//PS_P_HARDRADIUS - dx; + push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations particle1->x -= push; else - particle2->x += push; + particle2->x += push; } + push = pushamount; // reset push variable to 1 if (dy < HARDDIAMETER && dy > -HARDDIAMETER) { - - int32_t push; if (dy <= 0) - push = -1; //-(PS_P_HARDRADIUS + dy); // inverted push direction - else - push = 1; // PS_P_HARDRADIUS - dy; + push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction if (choice) // chose one of the particles to push, avoids oscillations particle1->y -= push; else - particle2->y += push; + particle2->y += push; } - //note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } - - } -// slow down particle by friction, the higher the speed, the higher the friction -void applyFriction(PSparticle *particle, uint8_t coefficient) +// slow down particle by friction, the higher the speed, the higher the friction coefficient must be <255 or friction is flipped +void applyFriction(PSparticle *particle, int32_t coefficient) { - if(particle->ttl) + //note: to increase calculation efficiency, coefficient is not checked if it is within necessary limits of 0-255! if coefficient is made < 1 particles speed up! + coefficient = (int32_t)255 - coefficient; + if (particle->ttl) { - particle->vx = ((int16_t)particle->vx * (255 - coefficient)) >> 8; - particle->vy = ((int16_t)particle->vy * (255 - coefficient)) >> 8; + particle->vx = ((int16_t)particle->vx * coefficient) >> 8; + particle->vy = ((int16_t)particle->vy * coefficient) >> 8; } } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 6b01ee9c73..8b275a07e9 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -39,15 +39,6 @@ //todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! //flags as bitfields is still very fast to access. - -union Flags { - struct { - - }; - uint8_t flagsByte; -}; - - //struct for a single particle typedef struct { int16_t x; //x position in particle system @@ -93,9 +84,9 @@ void Particle_Move_update(PSparticle *part, bool killoutofbounds = false, bool w void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness); void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); -void FireParticle_update(PSparticle *part, bool wrapX = false); +void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX = false); void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows); void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint8_t hardness); -void applyFriction(PSparticle *particle, uint8_t coefficient); +void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness); +void applyFriction(PSparticle *particle, int32_t coefficient); From add70511724b4397ccb064ae263e33784ace687c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 23 Mar 2024 20:11:48 +0100 Subject: [PATCH 08/21] bugfix --- wled00/FXparticleSystem.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 8b275a07e9..91156eb30d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -31,14 +31,11 @@ //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS 32 #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity - - -//todo: can add bitfields to add in more stuff, but accessing bitfields is slower than direct memory access! -//flags as bitfields is still very fast to access. //struct for a single particle typedef struct { int16_t x; //x position in particle system From 848d3d0263b71f394429ade3316c723691b4bde5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 24 Mar 2024 12:02:55 +0100 Subject: [PATCH 09/21] updated PS Fireworks with many changes and fine-tuning of parameters -removed spiral explosions -added more versatility to circular explosions -removed user selectable amount of rockets -tuned explosion size of circular explosions to match random explosions (more or less, may need improvement) -changed order of sliders in volcano animation --- wled00/FX.cpp | 237 ++++++++++++++++++-------------------- wled00/FXparticleSystem.h | 7 -- 2 files changed, 113 insertions(+), 131 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3620ae51a0..f54ec9e665 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8047,68 +8047,57 @@ static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rota * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ - uint16_t mode_particlefireworks(void) { - if (SEGLEN == 1) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions const uint32_t Max_x = (cols * PS_P_RADIUS - 1); const uint32_t Max_y = (rows * PS_P_RADIUS - 1); - #ifdef ESP8266 const uint32_t numParticles = 120; - const uint8_t MaxNumRockets = 2; #else - const uint32_t numParticles = 650; - const uint8_t MaxNumRockets = 8; + const uint32_t numParticles = 400; #endif - + const uint8_t numRockets = 4; PSparticle *particles; PSpointsource *rockets; - // allocate memory and divide it into proper pointers, max is 32k for all segments. uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (MaxNumRockets); + dataSize += sizeof(PSpointsource) * (numRockets); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - - // DEBUG_PRINT(F("particle datasize = ")); - // DEBUG_PRINTLN(dataSize); - rockets = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(rockets + MaxNumRockets); // cast the data array into a particle pointer - + particles = reinterpret_cast(rockets + numRockets); // cast the data array into a particle pointer uint32_t i = 0; - uint32_t j = 0; - uint8_t numRockets = min(uint8_t(1 + ((SEGMENT.custom3) >> 2)), MaxNumRockets); // 1 to 8 - + uint32_t j = 0; if (SEGMENT.call == 0) // initialization { for (i = 0; i < numParticles; i++) { particles[i].ttl = 0; } - for (i = 0; i < numRockets; i++) + for (j = 0; j < numRockets; j++) { - rockets[i].source.ttl = random16(20 * i); // first rocket starts immediately, others follow soon - rockets[i].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + rockets[j].source.ttl = random16(20 * j); // first rocket starts immediately, others follow soon + rockets[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } - - // update particles, create particles - uint8_t circularexplosion = random16(numRockets + 2); //choose a rocket by random (but not every round one will be picked) - uint8_t spiralexplosion = random16(numRockets + 2); - // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - uint16_t emitparticles; // number of particles to emit for each rocket's state + int32_t emitparticles; // number of particles to emit for each rocket's state i = 0; + // variables for circle explosions + uint32_t speed; + uint32_t currentspeed; + uint8_t angle; + uint32_t counter; + uint32_t angleincrement; + uint32_t speedvariation; + + bool circularexplosion = false; for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: @@ -8121,74 +8110,77 @@ uint16_t mode_particlefireworks(void) emitparticles = 0; } else // speed is zero, explode! - { - - #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion - #else - emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif - rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if (j == circularexplosion || j == spiralexplosion) // chosen rocket, do an angle emit (creating a circle) + { +#ifdef ESP8266 + emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion +#else + emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion +#endif + rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if(random16(4) == 0) //!!! make it 5 { - emitparticles = emitparticles >> 3; // emit less particles for circle-explosions - rockets[j].maxLife = 150; - rockets[j].minLife = 120; - rockets[j].var = 0; // speed variation around vx,vy (+/- var/2) + circularexplosion = true; + speed = 2 + random16(3); + currentspeed = speed; + counter = 0; + angleincrement = random16(20) + 10; + speedvariation = random16(3); + angle = random16(); // random start angle + // calculate the number of particles to make complete circles + int percircle = 256 / angleincrement + 2; + #ifdef ESP8266 //TODO: this line is untested on ESP8266 + int circles = (SEGMENT.intensity >> 7) + 1; // max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + #else + int circles = (SEGMENT.intensity >> 6) + 1;// max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); + #endif + emitparticles = percircle * circles; + rockets[j].var = 0; //no variation for a nice circle } } - - uint8_t speed = 3; - uint8_t angle = 0; - if (j == spiralexplosion) - angle = random16(8); - - while(i < numParticles) + for (i = 0; i < numParticles; i++) { if (particles[i].ttl == 0) { // particle is dead - - if (j == circularexplosion && emitparticles > 2) // do circle emit + if (emitparticles > 0) { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); - - if (angle > 242) // full circle completed, increase speed and reset angle + if (circularexplosion) // do circle emit { - angle += 10; - speed += 6; - rockets[j].source.hue = random16(); // new color for next row - rockets[j].source.sat = random16(); - if(emitparticles > 12) - emitparticles-=12; //emitted about 12 particles for one circle, ensures no incomplete circles are done + Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); + emitparticles--; + // set angle for next particle + angle += angleincrement; + counter++; + if (counter & 0x01) // make every second particle a lower speed + currentspeed = speed - speedvariation; else - emitparticles = 0; + currentspeed = speed; + if (counter > 256 / angleincrement + 2) // full circle completed, increase speed + { + counter = 0; + speed += 5; //increase speed to form a second circle + speedvariation = speedvariation<<1; //double speed variation + rockets[j].source.hue = random16(); // new color for next circle + rockets[j].source.sat = min((uint16_t)40,random16()); + } + } + else + { + Emitter_Fountain_emit(&rockets[j], &particles[i]); + emitparticles--; + if ((j % 3) == 0) + { + rockets[j].source.hue = random16(); // random color for each particle + rockets[j].source.sat = min((uint16_t)40, random16()); + } } - - // set angle for next particle - angle += 21; // about 30° - } - else if (j == spiralexplosion && emitparticles > 2) // do spiral emit - { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, speed); - emitparticles-=2; // only emit half as many particles as in circle explosion, it gets too huge otherwise - angle += 15; - speed++; - rockets[j].source.hue++; - rockets[j].source.sat = random16(155)+100; - - } - else if (emitparticles > 0) - { - Emitter_Fountain_emit(&rockets[j], &particles[i]); - emitparticles--; } else break; // done emitting for this rocket } - i++; } + circularexplosion = false; //reset for next rocket } - + // update particles for (i = 0; i < numParticles; i++) { @@ -8197,51 +8189,49 @@ uint16_t mode_particlefireworks(void) Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); } } - // update the rockets, set the speed state - for (i = 0; i < numRockets; i++) + for (j = 0; j < numRockets; j++) { - if (rockets[i].source.ttl) + if (rockets[j].source.ttl) + { + Particle_Move_update(&rockets[j].source); // move the rocket, age the rocket (ttl--) + } + else if (rockets[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { - Particle_Move_update(&rockets[i].source); // move the rocket, age the rocket (ttl--) - } - else if (rockets[i].source.vy > 0) - { // rocket has died and is moving up. stop it so it will explode (is handled in the code above) - rockets[i].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[i].source.hue = random16(); // random color - rockets[i].source.sat = random16(100)+155; - rockets[i].maxLife = 200; - rockets[i].minLife = 50; - rockets[i].source.ttl = random16((255 - SEGMENT.speed))+50; // standby time til next launch (in frames at 42fps, max of 300 is about 7 seconds - rockets[i].vx = 0; // emitting speed - rockets[i].vy = 0; // emitting speed - rockets[i].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) - } - else if (rockets[i].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + rockets[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + rockets[j].source.hue = random16(); // random color + rockets[j].source.sat = random16(100) + 155; + rockets[j].maxLife = 200; + rockets[j].minLife = 50; + rockets[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 50; // standby time til next launch + rockets[j].vx = 0; // emitting speed + rockets[j].vy = 3; // emitting speed + rockets[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) + } + else if (rockets[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - rockets[i].source.y = 1; // start from bottom - rockets[i].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[i].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[i].source.vx = random16(5) - 2; - rockets[i].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[i].source.sat = 30; // low saturation -> exhaust is off-white - rockets[i].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[i].maxLife = 30; // exhaust particle life - rockets[i].minLife = 10; - rockets[i].vx = 0; // emitting speed - rockets[i].vy = 0; // emitting speed - rockets[i].var = 6; // speed variation around vx,vy (+/- var/2) + rockets[j].source.y = 1; // start from bottom + rockets[j].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half + rockets[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height + rockets[j].source.vx = random16(5) - 2; + rockets[j].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) + rockets[j].source.sat = 30; // low saturation -> exhaust is off-white + rockets[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + rockets[j].maxLife = 30; // exhaust particle life + rockets[j].minLife = 10; + rockets[j].vx = 0; // emitting speed + rockets[j].vy = 0; // emitting speed + rockets[j].var = 6; // speed variation around vx,vy (+/- var/2) } } SEGMENT.fill(BLACK); // clear the matrix - // render the particles ParticleSys_render(particles, numParticles, false, false); - return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Height,Bounce,Rockets,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +//TODO: after implementing gravity function, add slider custom3 to set gravity force +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; /* * Particle Volcano (gravity spray) @@ -8319,23 +8309,22 @@ uint16_t mode_particlevolcano(void) { if (spray[i].source.vx > 0) // moving to the right currently { - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + spray[i].source.vx = SEGMENT.custom1 >> 4; // spray movingspeed } else { - spray[i].source.vx = -(SEGMENT.speed >> 4); // spray speed (is currently moving negative so keep it negative) + spray[i].source.vx = -(SEGMENT.custom1 >> 4); // spray speed (is currently moving negative so keep it negative) } } - else - { // wrap on the right side - spray[i].source.vx = SEGMENT.speed >> 4; // spray speed + else{ //wrap on the right side + spray[i].source.vx = SEGMENT.custom1 >> 4; // spray speed if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) } - spray[i].vy = SEGMENT.custom1 >> 2; // emitting speed, upward - spray[i].vx = 0; - spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.ttl = 255; // source never dies, replenish its lifespan + spray[i].vy = SEGMENT.speed >> 2; // emitting speed + spray[i].vx = 0; + spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + spray[i].source.ttl = 255; // source never dies, replenish its lifespan } i = 0; @@ -8378,7 +8367,7 @@ uint16_t mode_particlevolcano(void) ParticleSys_render(particles, numParticles, false, false); return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Move,Intensity,Speed,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=0,ix=160,c1=100,c2=160,c3=10,o1=1,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; //for debugging speed tests, this speed test function can be used, compiler will not optimize it void __attribute__((optimize("O0"))) SpeedTestfunction(void) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 91156eb30d..f4c2163757 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -66,13 +66,6 @@ typedef struct { #define GRAVITYCOUNTER 2 //the higher the value the lower the gravity (speed is increased every n'th particle update call), values of 1 to 4 give good results #define MAXGRAVITYSPEED 40 //particle terminal velocity -/* -//todo: make these local variables -uint8_t vortexspeed; //speed around vortex -uint8_t vortexdirection; //1 or 0 -int8_t vortexpull; //if positive, vortex pushes, if negative it pulls -*/ - void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed); From a6925631002ecd72760119031d2a44b0c93dbfa5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 4 Apr 2024 18:24:23 +0200 Subject: [PATCH 10/21] merge commit of dev branch, reworked the whole system into a class - put particle system in a class, went through the whole code, rewrote many of the functions - added local rendering buffer (renders to buffer in heap) - added fast and accurate color-add function - converted all FX to the class and improved all of them with parameter tuning and new features - fixed animation transitions with proper pointer setting in each FX call - fixed collisions - changed max number of particles and sprays based on some ram calculations - countless bugfixes - still a lot of debug stuff in the code, badly needs a cleanup --- wled00/FX.cpp | 2111 ++++++++++++++++------------------- wled00/FX.h | 11 +- wled00/FXparticleSystem.cpp | 1663 ++++++++++++++++----------- wled00/FXparticleSystem.h | 152 ++- 4 files changed, 2116 insertions(+), 1821 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f54ec9e665..9fb83d736b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -24,7 +24,7 @@ Modified heavily for WLED */ -// Information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata +// information for custom FX metadata strings: https://kno.wled.ge/interfaces/json-api/#effect-metadata #include "wled.h" #include "FX.h" @@ -6278,7 +6278,7 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale float *fftBin = nullptr; um_data_t *um_data; if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; + volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; samplePeak = *(uint8_t*) um_data->u_data[3]; @@ -7882,69 +7882,65 @@ uint16_t mode_2Dwavingcell() { static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; /* - * Particle System Candy (aka rotating sprays) + * Particle System Vortex * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) */ -uint16_t mode_particlerotatingspray(void) +uint16_t mode_particlevortex(void) { - if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 170; // maximum number of particles -#else - const uint32_t numParticles = 700; // maximum number of particles -#endif - - const uint8_t numSprays = 8; // maximum number of sprays - - PSparticle *particles; - PSpointsource *spray; - - // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer - + uint8_t numSprays; // maximum number of sprays + ParticleSystem *PartSys = NULL; uint32_t i = 0; uint32_t j = 0; uint8_t spraycount = 1 + (SEGMENT.custom1 >> 5); // number of sprays to display, 1-8 - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - SEGMENT.aux0 = 0; // starting angle + if (!initParticleSystem(PartSys)) + return mode_static(); // allocation failed; //allocation failed + + //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + // TODO: use SEGMENT.step for smooth direction change + numSprays = min(PartSys->numSources, (uint8_t) 8); for (i = 0; i < numSprays; i++) - { - spray[i].source.sat = 255; // set saturation - spray[i].source.x = (cols * PS_P_RADIUS) / 2; // center - spray[i].source.y = (rows * PS_P_RADIUS) / 2; // center - spray[i].source.vx = 0; - spray[i].source.vy = 0; - spray[i].maxLife = 400; - spray[i].minLife = 200; - spray[i].vx = 0; // emitting speed - spray[i].vy = 0; // emitting speed - spray[i].var = 0; // emitting variation + { + PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.x = (PartSys->maxX - PS_P_HALFRADIUS + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY - PS_P_HALFRADIUS + 1) >> 1; // center + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.vy = 0; + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800; //!!! + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].vy = 0; // emitting speed + PartSys->sources[i].var = 0; // emitting variation + if (SEGMENT.check1) // random color is checked + PartSys->sources[i].source.hue = random16(); + else + { + uint8_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; + } } + PartSys->setKillOutOfBounds(true); } - + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + //DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSprays = min(PartSys->numSources, (uint8_t)8); + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change { if (SEGMENT.check1) @@ -7956,46 +7952,23 @@ uint16_t mode_particlerotatingspray(void) { if (SEGMENT.check1) // random color is checked { - spray[i].source.hue = random16(); + PartSys->sources[i].source.hue = random16(); } else { uint8_t coloroffset = 0xFF / spraycount; - spray[i].source.hue = coloroffset * i; - } - } - } - - uint8_t percycle = spraycount; // maximum number of particles emitted per cycle - - #ifdef ESP8266 - if (SEGMENT.call & 0x01) //every other frame, do not emit to save particles - percycle = 0; -#endif - i = 0; - j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. - while (i < numParticles) - { - if (particles[i].ttl == 0) // find a dead particle - { - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Fountain_emit(&spray[j], &particles[i]); - j = (j + 1) % spraycount; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted + PartSys->sources[i].source.hue = coloroffset * i; } } - i++; } - //set rotation direction and speed - int32_t rotationspeed = SEGMENT.speed << 2; - bool direction = SEGMENT.check2; + // set rotation direction and speed TODO: use SEGMENT.step to increment until speed is reached, increment speed depends on rotation speed as well fast rotations speed up faster too + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag if (SEGMENT.custom2 > 0) // automatic direction change enabled { - uint16_t changeinterval = (265 - SEGMENT.custom2); + uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval @@ -8013,33 +7986,48 @@ uint16_t mode_particlerotatingspray(void) } } + int32_t currentspeed = (int32_t)SEGMENT.step; //make a signed integer out of step + int32_t speedincrement = 20 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); if (direction) - SEGMENT.aux0 += rotationspeed << 2; + { + if(currentspeed < (SEGMENT.speed << 2)) //speed is not on target speed yet + currentspeed += speedincrement; + } else - SEGMENT.aux0 -= rotationspeed << 2; + { + if (currentspeed > -(SEGMENT.speed << 2)) // speed is not on target speed yet + currentspeed -= speedincrement; + } + SEGMENT.aux0 += currentspeed; + SEGMENT.step = (uint32_t)currentspeed; //save it back // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; - for (i = 0; i < spraycount; i++) + for (j = 0; j < spraycount; j++) { // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - spray[i].vx = (cos16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].vy = (sin16(SEGMENT.aux0 + angleoffset * i) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - spray[i].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) } - for (i = 0; i < numParticles; i++) +//TODO: limit the emit amount by particle speed. should not emit more than one for every speed of like 20 or so, it looks weird on initialisation also make it depnd on angle speed, emit no more than once every few degrees -> less overlap (need good finetuning) + + j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) { - Particle_Move_update(&particles[i], true); // move the particles, kill out of bounds particles + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->sprayEmit(PartSys->sources[j]); + j = (j + 1) % spraycount; } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + PartSys->update(); //update all particles and render to frame return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rotation Speed,Particle Speed,Arms,Flip Speed,Nozzle,Random Color, Direction, Random Flip;;!;012;pal=56,sx=18,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* * Particle Fireworks @@ -8047,536 +8035,402 @@ static const char _data_FX_MODE_PARTICLEROTATINGSPRAY[] PROGMEM = "PS Candy@Rota * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ + uint16_t mode_particlefireworks(void) { if (SEGLEN == 1) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - const uint32_t Max_y = (rows * PS_P_RADIUS - 1); -#ifdef ESP8266 - const uint32_t numParticles = 120; -#else - const uint32_t numParticles = 400; -#endif - const uint8_t numRockets = 4; - PSparticle *particles; - PSpointsource *rockets; - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numRockets); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - rockets = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(rockets + numRockets); // cast the data array into a particle pointer + ParticleSystem *PartSys = NULL; + uint8_t numRockets; uint32_t i = 0; - uint32_t j = 0; - if (SEGMENT.call == 0) // initialization + uint32_t j = 0; + + if (SEGMENT.call == 0) // initialization { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(100); //ground bounce is fixed + numRockets = min(PartSys->numSources, (uint8_t)4); for (j = 0; j < numRockets; j++) { - rockets[j].source.ttl = random16(20 * j); // first rocket starts immediately, others follow soon - rockets[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numRockets = min(PartSys->numSources, (uint8_t)4); + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed + PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time - int32_t emitparticles; // number of particles to emit for each rocket's state - i = 0; + uint32_t emitparticles; // number of particles to emit for each rocket's state + // variables for circle explosions - uint32_t speed; - uint32_t currentspeed; - uint8_t angle; - uint32_t counter; - uint32_t angleincrement; - uint32_t speedvariation; - + uint8_t speed; + uint8_t currentspeed; + uint16_t angle; + uint8_t counter; + uint16_t angleincrement; + uint8_t percircle; + uint8_t speedvariation; bool circularexplosion = false; for (j = 0; j < numRockets; j++) { // determine rocket state by its speed: - if (rockets[j].source.vy > 0) + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust emitparticles = 1; } - else if (rockets[j].source.vy < 0) + else if (PartSys->sources[j].source.vy < 0) { // falling down emitparticles = 0; } else // speed is zero, explode! { -#ifdef ESP8266 + #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion -#else + #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion -#endif - rockets[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch - if(random16(4) == 0) //!!! make it 5 + #endif + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + if(random16(4) == 0) { circularexplosion = true; speed = 2 + random16(3); currentspeed = speed; counter = 0; - angleincrement = random16(20) + 10; - speedvariation = random16(3); + angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) angle = random16(); // random start angle + speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles - int percircle = 256 / angleincrement + 2; - #ifdef ESP8266 //TODO: this line is untested on ESP8266 - int circles = (SEGMENT.intensity >> 7) + 1; // max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); - #else - int circles = (SEGMENT.intensity >> 6) + 1;// max(4, (int)min((int32_t)1, (emitparticles>>2) / percircle)); - #endif + percircle = (uint16_t)0xFFFF / angleincrement + 1; + int circles = (SEGMENT.intensity >> 6) + 1; emitparticles = percircle * circles; - rockets[j].var = 0; //no variation for a nice circle + PartSys->sources[j].var = 0; //no variation for nicer circles } } - for (i = 0; i < numParticles; i++) + for (i = 0; i < emitparticles; i++) { - if (particles[i].ttl == 0) - { // particle is dead - if (emitparticles > 0) + if (circularexplosion) // do circle emit + { + if (counter & 0x01) // make every second particle a lower speed + currentspeed = speed - speedvariation; + else + currentspeed = speed; + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); //note: compiler warnings can be ignored, variables are set just above + counter++; + if (counter > percircle) // full circle completed, increase speed { - if (circularexplosion) // do circle emit - { - Emitter_Angle_emit(&rockets[j], &particles[i], angle, currentspeed); - emitparticles--; - // set angle for next particle - angle += angleincrement; - counter++; - if (counter & 0x01) // make every second particle a lower speed - currentspeed = speed - speedvariation; - else - currentspeed = speed; - if (counter > 256 / angleincrement + 2) // full circle completed, increase speed - { - counter = 0; - speed += 5; //increase speed to form a second circle - speedvariation = speedvariation<<1; //double speed variation - rockets[j].source.hue = random16(); // new color for next circle - rockets[j].source.sat = min((uint16_t)40,random16()); - } - } - else - { - Emitter_Fountain_emit(&rockets[j], &particles[i]); - emitparticles--; - if ((j % 3) == 0) - { - rockets[j].source.hue = random16(); // random color for each particle - rockets[j].source.sat = min((uint16_t)40, random16()); - } - } + counter = 0; + speed += 5; //increase speed to form a second circle + speedvariation = speedvariation ? speedvariation + random16(4) : 0; // double speed variation + PartSys->sources[j].source.hue = random16(); // new color for next circle + PartSys->sources[j].source.sat = min((uint16_t)150,random16()); + } + angle += angleincrement; // set angle for next particle + } + else + { + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) + { + PartSys->sources[j].source.hue = random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + // PartSys->sources[j].source.sat = min((uint16_t)150, random16()); //dont change saturation, this can also be exhaust! } - else - break; // done emitting for this rocket } } + if(i == 0) //no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) circularexplosion = false; //reset for next rocket } - - // update particles - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl) - { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, SEGMENT.custom2); - } - } + // update the rockets, set the speed state for (j = 0; j < numRockets; j++) { - if (rockets[j].source.ttl) + if (PartSys->sources[j].source.ttl) { - Particle_Move_update(&rockets[j].source); // move the rocket, age the rocket (ttl--) + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->particlesettings); //todo: need different settings for rocket? } - else if (rockets[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) + else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { - rockets[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket - rockets[j].source.hue = random16(); // random color - rockets[j].source.sat = random16(100) + 155; - rockets[j].maxLife = 200; - rockets[j].minLife = 50; - rockets[j].source.ttl = random16((1024 - ((uint32_t)SEGMENT.speed<<2))) + 50; // standby time til next launch - rockets[j].vx = 0; // emitting speed - rockets[j].vy = 3; // emitting speed - rockets[j].var = (SEGMENT.intensity >> 3) + 10; // speed variation around vx,vy (+/- var/2) - } - else if (rockets[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.vy = 0; // set speed to zero so code above will recognize this as an exploding rocket + PartSys->sources[j].source.hue = random16(); // random color + PartSys->sources[j].source.sat = random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = random16((1200 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 3) + 10) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd nubmers + } + else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - rockets[j].source.y = 1; // start from bottom - rockets[j].source.x = (rand() % (Max_x >> 1)) + (Max_y >> 2); // centered half - rockets[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket height - rockets[j].source.vx = random16(5) - 2; - rockets[j].source.hue = 30; // rocket exhaust = orange (if using rainbow palette) - rockets[j].source.sat = 30; // low saturation -> exhaust is off-white - rockets[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - rockets[j].maxLife = 30; // exhaust particle life - rockets[j].minLife = 10; - rockets[j].vx = 0; // emitting speed - rockets[j].vy = 0; // emitting speed - rockets[j].var = 6; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half + PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse + PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) + PartSys->sources[j].maxLife = 30; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = 0; // emitting speed + PartSys->sources[j].var = 5; // speed variation around vx,vy (+/- var/2) } } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + + PartSys->update(); // update and render return FRAMETIME; } -//TODO: after implementing gravity function, add slider custom3 to set gravity force -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Bounce,,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=50,c1=64,c2=128,c3=10,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* - * Particle Volcano (gravity spray) + * Particle Volcano * Particles are sprayed from below, spray moves back and forth if option is set * Uses palette for particle color * by DedeHai (Damian Schneider) */ - uint16_t mode_particlevolcano(void) { - if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - - // particle system x dimension - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 100; // maximum number of particles -#else - const uint32_t numParticles = 450; // maximum number of particles -#endif - - const uint8_t numSprays = 1; - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSpointsource *spray; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer - + ParticleSystem *PartSys = NULL; + uint8_t numSprays; //note: so far only one tested but more is possible uint32_t i = 0; - uint32_t j = 0; - - if (SEGMENT.call == 0) // initialization - { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - for (i = 0; i < numSprays; i++) - { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); - spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 0; - spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 20; - spray[i].source.collide = true; //seeded particles will collide - spray[i].vx = 0; // emitting speed - spray[i].vy = 20; // emitting speed - // spray.var = 10 + (random16() % 4); - } - } - // change source emitting color from time to time - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setBounceY(true); + PartSys->enableGravity(true); //enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + numSprays = min(PartSys->numSources, (uint8_t)1); //number of sprays for (i = 0; i < numSprays; i++) { - spray[i].source.hue++; // = random16(); //change hue of spray source - // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - if (SEGMENT.check2) // bounce - { - if (spray[i].source.vx > 0) // moving to the right currently - { - spray[i].source.vx = SEGMENT.custom1 >> 4; // spray movingspeed - } - else - { - spray[i].source.vx = -(SEGMENT.custom1 >> 4); // spray speed (is currently moving negative so keep it negative) - } - } - else{ //wrap on the right side - spray[i].source.vx = SEGMENT.custom1 >> 4; // spray speed - if (spray[i].source.x >= Max_x - 32) //compiler warning can be ignored, source.x is always > 0 - spray[i].source.x = 1; // wrap if close to border (need to wrap before the bounce updated detects a border collision or it will just be stuck) - } - spray[i].vy = SEGMENT.speed >> 2; // emitting speed - spray[i].vx = 0; - spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.ttl = 255; // source never dies, replenish its lifespan - } - - i = 0; - j = 0; - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl == 0) // find a dead particle - { - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Fountain_emit(&spray[j], &particles[i]); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].vx = 0; // emitting speed } } - uint8_t hardness = SEGMENT.custom2; - if (SEGMENT.check3) // collisions enabled - detectCollisions(particles, numParticles, hardness); - - for (i = 0; i < numSprays; i++) - { - Particle_Bounce_update(&spray[i].source, (uint8_t)255); // move the source - } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - for (i = 0; i < numParticles; i++) + if (PartSys == NULL) { - // set color according to ttl ('color by age') - if (SEGMENT.check1) - particles[i].hue = min((uint16_t)220, particles[i].ttl); - - Particle_Gravity_update(&particles[i], false, SEGMENT.check2, true, hardness); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } - SEGMENT.fill(BLACK); // clear the matrix + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - // render the particles - ParticleSys_render(particles, numParticles, false, false); - return FRAMETIME; -} -static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Size,Color by Age,Walls,Collisions;;!;012;pal=35,sx=100,ix=160,c1=0,c2=160,c3=10,o1=1,o2=0,o3=0"; + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); -//for debugging speed tests, this speed test function can be used, compiler will not optimize it -void __attribute__((optimize("O0"))) SpeedTestfunction(void) -{ - // unmodifiable compiler code - Serial.print("Speedtest: "); - int32_t i; - volatile int32_t randomnumber; - uint32_t start = micros(); - uint32_t time; - volatile int32_t windspeed; - for (i = 0; i < 100000; i++) - { - //windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random8(); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); - start = micros(); - for (i = 0; i < 100000; i++) - { - //windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random8(15); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); - start = micros(); - for (i = 0; i < 100000; i++) - { - // windspeed=(inoise16(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random16(); - } - time = micros() - start; - Serial.print(time); - Serial.print(" "); - start = micros(); - for (i = 0; i < 100000; i++) - { - // windspeed = (inoise8(SEGMENT.aux0, start >> 2) - 127) / ((271 - SEGMENT.custom2) >> 4); - randomnumber = random16(15); + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles (and update the sources) + { + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing + PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers + PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan + // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + //Serial.println("emit"); + } } - - time = micros() - start; - Serial.print(time); - Serial.print(" "); - Serial.println(" ***"); + PartSys->update(); // update and render + return FRAMETIME; } +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; -/* - * Particle Fire - * realistic fire effect using particles. heat based and using perlin-noise for wind - * by DedeHai (Damian Schneider) - */ - + /* + * Particle Fire + * realistic fire effect using particles. heat based and using perlin-noise for wind + * by DedeHai (Damian Schneider) + */ uint16_t mode_particlefire(void) { - - // speed tests : - // SpeedTestfunction(); - if (SEGLEN == 1) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - - // particle system box dimensions - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - - #ifdef ESP8266 - const uint32_t numFlames = min((uint32_t)12, (cols<<1)); // limit to 18 flames, not enough ram on ESP8266 - const uint32_t numParticles = numFlames * 15; //limit number of particles to about 180 or ram will be depleted - const uint32_t numNormalFlames = numFlames - (numFlames / 3); // number of normal flames, rest of flames are baseflames - uint32_t percycle = numFlames >> 2 ;// maximum number of particles emitted per cycle - #else - const uint32_t numFlames = (cols << 1); // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results, add a few for the base flames - const uint32_t numParticles = numFlames * 18; - const uint32_t numNormalFlames = numFlames - (cols / 3); // number of normal flames, rest of flames are baseflames - uint32_t percycle = numFlames / 3; // maximum number of particles emitted per cycle -#endif - - - PSparticle *particles; - PSpointsource *flames; - - // allocate memory and divide it into proper pointers - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numFlames); - - if (!SEGENV.allocateData(dataSize)) - { - return mode_static(); // allocation failed; //allocation failed - } - - flames = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(flames + numFlames); // cast the data array into a particle pointer - - uint32_t i; + ParticleSystem *PartSys = NULL; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { + if (!initParticleSystem(PartSys)) + return mode_static(); // allocation failed; //allocation failed + Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise - // make sure all particles start out dead - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - // initialize the flame sprays + numFlames = PartSys->numSources; for (i = 0; i < numFlames; i++) { - flames[i].source.ttl = 0; - flames[i].source.vx = 0; // emitter moving speed; - flames[i].source.vy = 0; + PartSys->sources[i].source.ttl = 0; + PartSys->sources[i].source.vx = 0; // emitter moving speed; + PartSys->sources[i].source.vy = 0; // note: other parameters are set when creating the flame (see blow) } + + DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } + // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? + PartSys->setWrapX(SEGMENT.check2); + // update the flame sprays: for (i = 0; i < numFlames; i++) { - if (flames[i].source.ttl > 0) + if (PartSys->sources[i].source.ttl > 0) { - flames[i].source.ttl--; + PartSys->sources[i].source.ttl--; } else // flame source is dead { // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - if (SEGMENT.check1) - { // wrap around in X direction, distribute randomly - flames[i].source.x = rand() % Max_x; - } - else // no X-wrapping - { - flames[i].source.x = (rand() % (Max_x - (PS_P_RADIUS * ((cols>>3)+1)))) + PS_P_RADIUS * ((cols>>4)+1); // distribute randomly but not close to the corners (cannot use random16() it is not random enough, tends to burn on left side) - } + { + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width } - flames[i].source.y = -PS_P_RADIUS; // set the source below the frame - - if (i < numNormalFlames) - { - flames[i].source.ttl = random16((SEGMENT.intensity * SEGMENT.intensity) >> 9) / (1 + (SEGMENT.speed >> 6)) + 10; //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed - flames[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 4; - flames[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) - flames[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) - flames[i].var = random16(5) + 3;; // speed variation around vx,vy (+/- var/2) - } - else - { // base flames to make the base brighter, flames are slower and short lived - flames[i].source.ttl = random16(25) + 15; // lifetime of one flame - flames[i].maxLife = 25; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height - flames[i].minLife = 12; - flames[i].vx = 0; // emitting speed, sideways - flames[i].vy = (SEGMENT.custom1 >> 4); // slow emitting speed (upwards) - flames[i].var = 5; // speed variation around vx,vy (+/- var/2) - } + PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame + //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //old not really good, too intense + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! + //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (SEGMENT.speed >> 5)); // this is experimental, fine tuning all parameters + PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = 4; + PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) -> this is good + //PartSys->sources[i].var = (random16(5) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (random16(2 + (SEGMENT.speed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } + } - - if (SEGMENT.call & 0x01) //update noise position every second frames + + if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation if (SEGMENT.call & 0x02) //every tird frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often + + // add wind force to all particles + int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 12; + PartSys->applyForce(PartSys->particles, PartSys->usedParticles, windspeed, 0); } - int8_t windspeed = (int8_t)(inoise8(SEGMENT.aux0,SEGMENT.aux1) - 127) / ((271 - SEGMENT.custom2) >> 4); + SEGMENT.step++; - // update particles, create particles - uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if perCycle is smaller than number of flames) - for (i = 0; i < numParticles; i++) +//this is a work in progress... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds + if (SEGMENT.check3) { - if (particles[i].ttl == 0) + if (SEGMENT.call % map(SEGMENT.speed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame + { + for (i = 0; i < PartSys->usedParticles; i++) { - if(percycle > 0) + //if (PartSys->particles[i].y > (PS_P_RADIUS << 1) && PartSys->particles[i].y < PartSys->maxY - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere + if (PartSys->particles[i].y < PartSys->maxY/4)// - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere -> bottom quarter seems a good balance { - Emitter_Flame_emit(&flames[j], &particles[i]); - j++; - if (j >= numFlames) - { // or simpler: j=j%numFlames; but that is slow on ESP8266 - j = 0; - } - percycle--; + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x , PartSys->particles[i].y >> 1, SEGMENT.step<<2 ) - 127); + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); + int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! + + //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x>>1, SEGMENT.step<<5) - 127); + // curl = ((curl * PartSys->particles[i].y) / PartSys->maxY); //'curl' stronger at the top + //int modulation = inoise8(SEGMENT.step<<3, SEGMENT.aux1) ; + //PartSys->particles[i].vx += curl>>2; + //PartSys->particles[i].vy += curl>>3; + //PartSys->particles[i].vx += (curl * ((SEGMENT.custom2 * modulation)>>7)) >> 9; + //PartSys->particles[i].vy += ((curl ) * ((SEGMENT.custom2 * modulation)>>7))>>10; + //PartSys->particles[i].vx += (curl * curl * (SEGMENT.speed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong + PartSys->particles[i].vx += (curl * (SEGMENT.speed + 10)) >> 9; //-> this is not too bad! + // PartSys->particles[i].vy += (curl * SEGMENT.custom2 ) >> 13; } } - else if (particles[i].y > PS_P_RADIUS) // particle is alive, apply wind if y > 1 - { - // add wind using perlin noise - particles[i].vx = windspeed; - } + } } - FireParticle_update(particles, numParticles, SEGMENT.check1); // update particle, use X-wrapping if check 1 is set by user - - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_renderParticleFire(particles, numParticles, SEGMENT.check1); // draw matrix + uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + for(i=0; i < percycle; i++) + { + PartSys->flameEmit(PartSys->sources[j]); + j = (j + 1) % numFlames; + } +/* + j=5; + //a test: emitting one base particle per frame + if (SEGMENT.check1) + { + for (i = 0; i < PartSys->usedParticles; i++) // emit particles //todo: if this works, make it the last spray + { + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].vy = 1 ;//+ (SEGMENT.speed >> 3); + PartSys->particles[i].ttl = 10;//(PS_P_RADIUS<<2) / PartSys->particles[i].vy; + PartSys->particles[i].x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; + PartSys->particles[i].y = 0; + PartSys->particles[i].vx = 0;//(((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1) + 5) >> 1; // side speed is +/- a quarter of the custom1 slider + Serial.print("*"); + j--; + } + if(j==0) break; + } + Serial.println("B"); + }*/ + + + PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Flames,Wind,Color Scheme, Cylinder;;!;012;sx=130,ix=120,c1=110,c2=128,c3=0,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8585,428 +8439,351 @@ this is quite versatile, can be made to look like rain or snow or confetti etc. Uses palette for particle color by DedeHai (Damian Schneider) */ - -uint16_t mode_particlefall(void) +uint16_t mode_particlepit(void) { - if (SEGLEN == 1) return mode_static(); + ParticleSystem *PartSys = NULL; - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 100; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - PSparticle *particles; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - - uint32_t i = 0; - - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys)) //init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->enableGravity(true); + PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if zero + if (PartSys == NULL) { - while (i < numParticles) // emit particles + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + if (SEGMENT.custom2>0) + { + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + } + else{ + PartSys->enableParticleCollisions(false); + } + + uint32_t i; // index variable + + if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero { - if (particles[i].ttl == 0) // find a dead particle + for (i = 0; i < PartSys->usedParticles; i++) // emit particles { - // emit particle at random position just over the top of the matrix - particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - - if (random16(5) == 0) // 16% of particles apper anywhere - particles[i].x = random16(cols * PS_P_RADIUS - 1); - else // rest is emitted at center half - particles[i].x = random16((cols >> 1) * PS_P_RADIUS + (cols >> 1) * PS_P_RADIUS); - - particles[i].y = random16(rows * PS_P_RADIUS) + rows * PS_P_RADIUS; // particles appear somewhere above the matrix, maximum is double the height - particles[i].vx = (((int16_t)random16(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)) >> 1; // side speed is +/- a quarter of the custom1 slider - particles[i].vy = -(SEGMENT.speed >> 1); - particles[i].hue = random16(); // set random color - particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - particles[i].collide = true; // particle will collide - break; //emit only one particle per round + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation + PartSys->particles[i].collide = true; //enable collision for particle + break; // emit only one particle per round + } } - i++; } - } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - detectCollisions(particles, numParticles, hardness); - - // now move the particles - uint32_t frictioncoefficient = 1; - if (SEGMENT.speed < 50) - { + if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; - } - - for (i = 0; i < numParticles; i++) - { - // apply 'air friction' to smooth things out, slows down all particles depending on their speed - applyFriction(&particles[i], frictioncoefficient); - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, min(hardness,(uint8_t)200)); // surface hardness max is 200 - } - SEGMENT.fill(BLACK); // clear the matrix + if (SEGMENT.call % (3+(SEGMENT.custom2>>2)) == 0) + PartSys->applyFriction(frictioncoefficient); - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation, from 7 to 255, 7 is close enough to white (for snow for example) + PartSys->update(); // update and render - return FRAMETIME; -} -static const char _data_FX_MODE_PARTICLEFALL[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;012;pal=11,sx=100,ix=200,c1=31,c2=100,c3=28,o1=0,o2=0,o3=1"; + return FRAMETIME; + } +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall * Uses palette for particle color, spray source at top emitting particles, many config options * by DedeHai (Damian Schneider) */ - uint16_t mode_particlewaterfall(void) { - if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 100; // maximum number of particles - const uint8_t numSprays = 1; -#else - const uint32_t numParticles = 500; // maximum number of particles - const uint8_t numSprays = 2; -#endif - - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSpointsource *spray; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer - + ParticleSystem *PartSys = NULL; + uint8_t numSprays; uint32_t i = 0; - uint32_t j = 0; - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->enableGravity(true); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS) / 2 - PS_P_RADIUS + 2 * PS_P_RADIUS * (i); - spray[i].source.y = (rows + 4) * (PS_P_RADIUS * (i + 1)); // source y position, few pixels above the top to increase spreading before entering the matrix - spray[i].source.vx = 0; - spray[i].source.collide = true; // seeded particles will collide - #ifdef ESP8266 - spray[i].maxLife = 100; // lifetime in frames - spray[i].minLife = 50; - #else - spray[i].maxLife = 400; // lifetime in frames - spray[i].minLife = 150; - #endif - - spray[i].vx = 0; // emitting speed - spray[i].var = 7; // emiting variation + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.collide = true; // seeded particles will collide +#ifdef ESP8266 + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; +#else + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; +#endif + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].var = 7; // emiting variation } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - // change source emitting color - for (i = 0; i < numSprays; i++) + if (PartSys == NULL) { - spray[i].source.hue++; // change hue of spray source + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (uint32_t)2)); // number of sprays depends on segment width + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + { + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions } - uint8_t intensity = SEGMENT.intensity; - - if (SEGMENT.call % (9 - (intensity >> 5)) == 0 && intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue++; //change hue of spray source + } + if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero + { for (i = 0; i < numSprays; i++) { - spray[i].vy = -SEGMENT.speed >> 3; // emitting speed, down - spray[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (cols - 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position - spray[i].source.ttl = 255; // source never dies, replenish its lifespan - spray[i].var = SEGMENT.custom1 >> 3; // emiting variation 0-32 - } - - i = 0; - j = 0; - while (i < numParticles) - { - if (particles[i].ttl == 0) // find a dead particle - { - Emitter_Fountain_emit(&spray[j], &particles[i]); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } - } - i++; + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + //PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan TODO: source is not moved? + PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers } - } - - // detect and handle collisions - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - - if (SEGMENT.custom2 > 0) // switch off collisions if hardnes is set to zero - { - detectCollisions(particles, numParticles, hardness); - } - else - { - hardness = 150; // set hardness (for ground bounce) to fixed value if not using collisions - } - // now move the particles - for (i = 0; i < numParticles; i++) - { - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.call % 8 == 0) + for (i = 0; i < numSprays; i++) { - applyFriction(&particles[i], 1); + PartSys->sprayEmit(PartSys->sources[i]); } - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;012;pal=9,sx=15,ix=200,c1=15,c2=128,c3=17,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collisions,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=60,c2=160,c3=17,o1=0,o2=0,o3=1"; /* Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) Uses palette for particle color by DedeHai (Damian Schneider) */ - uint16_t mode_particlebox(void) { + if (SEGLEN == 1) return mode_static(); + ParticleSystem *PartSys = NULL; + uint32_t i; - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys)) // init + return mode_static(); // allocation failed; //allocation failed + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -#ifdef ESP8266 - const uint32_t numParticles = 80; // maximum number of particles -#else - const uint32_t numParticles = 255; // maximum number of particles -#endif + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! + } - PSparticle *particles; + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint32_t i = 0; + #ifdef ESP8266 + uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + #else + uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); + #endif + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization of particles (cannot be done in above loop, only if code lines above here are copied there) { - SEGMENT.aux0 = rand(); // position (either in noise or in sine function) - for (i = 0; i < numParticles; i++) + SEGMENT.aux0 = rand(); // position in perlin noise + for (i = 0; i < maxnumParticles; i++) { - particles[i].ttl = 500; // all particles are alive (but not all are calculated/rendered) - particles[i].hue = i * 3; // full color range (goes over palette colors three times so it is also colorful when using fewer particles) - particles[i].sat = 255; // set full saturation (lets palette choose the color) - particles[i].x = map(i, 0, 255, 1, cols * PS_P_RADIUS); // distribute along x according to color - particles[i].y = random16((rows >> 2) * PS_P_RADIUS); // in the bottom quarder - particles[i].collide = true; // all particles collide + PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].hue = i * 5; // color range + PartSys->particles[i].sat = 255; // set full saturation (lets palette choose the color) + PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction + PartSys->particles[i].collide = true; // all particles collide } } - uint16_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); - - i = 0; - if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { - int32_t xgravity; int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + if(SEGMENT.check2) //direction + SEGMENT.aux0 += increment; // update counter + else + SEGMENT.aux0 -= increment; - SEGMENT.aux0 += (SEGMENT.speed >> 6) + 1; // update position in noise - - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); //TODO: inoise 16 would be faster - ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - if (SEGMENT.check1) // sloshing, y force is alwys downwards + if(SEGMENT.check1) //random, use perlin noise { - if (ygravity > 0) + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); // TODO: inoise 16 would be faster? + ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); + // scale the gravity force down + xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) + ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); + } + else //go in a circle + { + // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle + xgravity = ((int32_t)(SEGMENT.custom1 >> 3) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1 >> 3) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) //sloshing, y force is alwys downwards + { + if(ygravity > 0) ygravity = -ygravity; } - // scale the gravity force down - xgravity /= 16; - ygravity /= 16; - - for (i = 0; i < numParticles; i++) + PartSys->applyForce(PartSys->particles, PartSys->usedParticles, xgravity, ygravity); + + // reset particle TTL so they never die + for (i = 0; i < PartSys->usedParticles; i++) { - if (particles[i].ttl > 0) + if (PartSys->particles[i].ttl > 0) { - particles[i].vx += xgravity; - particles[i].vy += ygravity; - particles[i].ttl = 500; // particles never die + PartSys->particles[i].ttl = 500; // particles never die } } } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - detectCollisions(particles, displayparticles, hardness); - - // now move the particles - for (i = 0; i < displayparticles; i++) - { - particles[i].ttl = 500; // particles never die - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.call % 8 == 0) - { - applyFriction(&particles[i], 1); - } - Particle_Bounce_update(&particles[i], hardness); - } + if (SEGMENT.call % (32-SEGMENT.custom3) == 0) + PartSys->applyFriction(2); - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, displayparticles, false, false); + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt strength,Hardness,,Sloshing;;!;012;pal=1,sx=120,ix=100,c1=190,c2=210,o1=0"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=43,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above calculates slope gradient at the particle positions and applies 'downhill' force, restulting in a fuzzy perlin noise display by DedeHai (Damian Schneider) */ - uint16_t mode_particleperlin(void) { - if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 80; -#else - const uint32_t numParticles = 350; -#endif - - PSparticle *particles; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - - uint32_t i = 0; - - if (SEGMENT.call == 0) // initialization + ParticleSystem *PartSys = NULL; + uint32_t i; + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { + if (!initParticleSystem(PartSys)) // init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); - for (i = 0; i < numParticles; i++) + for (i = 0; i < PartSys->numParticles; i++) { - particles[i].ttl = random16(500) + 200; - particles[i].x = random16(cols * PS_P_RADIUS); - particles[i].y = random16(rows * PS_P_RADIUS); - particles[i].sat = 255; // full saturation, color set by palette + PartSys->particles[i].sat = 255; // full saturation, color set by palette + PartSys->particles[i].collide = true; // all particles colllide } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, numParticles); + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setBounceY(true); + PartSys->setWallHardness(SEGMENT.custom1); //wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); + PartSys->setUsedParticles(displayparticles); // apply 'gravity' from a 2D perlin noise map - SEGMENT.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position // update position in noise for (i = 0; i < displayparticles; i++) { - - if (particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + if (PartSys->particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) { - particles[i].ttl = random16(500) + 200; - particles[i].x = random16(cols * PS_P_RADIUS); - particles[i].y = random16(rows * PS_P_RADIUS); + PartSys->particles[i].ttl = random16(500) + 200; + PartSys->particles[i].x = random16(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); } - int32_t xnoise = particles[i].x / (1 + (SEGMENT.custom3 >> 1)); // position in perlin noise, scaled by slider - int32_t ynoise = particles[i].y / (1 + (SEGMENT.custom3 >> 1)); - + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position - particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 6 == 0) // do not apply the force every frame, is too chaotic - { - int16_t xslope = baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0); - int16_t yslope = baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0); - - particles[i].vx += xslope >> 1; // slope is about 0-8 - particles[i].vy += yslope >> 1; - } - } - - // move particles - for (i = 0; i < displayparticles; i++) - { - // apply a bit of friction so particles are less jittery - if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) // need to apply friction very rarely or particles will clump - applyFriction(&particles[i], 1); - - Particle_Bounce_update(&particles[i], 255); + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 10 == 0) // do not apply the force every frame, is too chaotic + { + int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); + int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); + PartSys->applyForce(&(PartSys->particles[i]), 1, xslope, yslope); + } } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, displayparticles, false, false); + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,,Friction,Scale;;!;012;pal=54,sx=70;ix=200,c1=120,c2=120,c3=4,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=255,c2=180,c3=20,o1=0"; /* - * Particle smashing down like meteorites and exploding as they hit the ground, has many parameters to play with + * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ @@ -9014,165 +8791,128 @@ uint16_t mode_particleimpact(void) { if (SEGLEN == 1) return mode_static(); - - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle system box dimensions - const uint32_t Max_x(cols * PS_P_RADIUS - 1); - const uint32_t Max_y(rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 150; - const uint8_t MaxNumMeteors = 2; -#else - const uint32_t numParticles = 550; - const uint8_t MaxNumMeteors = 8; -#endif - - PSparticle *particles; - PSpointsource *meteors; - - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (MaxNumMeteors); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - meteors = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(meteors + MaxNumMeteors); // cast the data array into a particle pointer - + ParticleSystem *PartSys = NULL; uint32_t i = 0; - uint32_t j = 0; - uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + uint8_t MaxNumMeteors; + PSsettings meteorsettings = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled (todo: if ESP8266 is ok with out of bounds particles, this can be removed, it just takes care of the kill out of bounds setting) - if (SEGMENT.call == 0) // initialization + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) + PartSys->enableGravity(true); + PartSys->setBounceY(true); //always use ground bounce + // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); for (i = 0; i < MaxNumMeteors; i++) { - meteors[i].source.y = 10; - meteors[i].source.ttl = random16(20 * i); // set initial delay for meteors - meteors[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - meteors[i].source.sat = 255; // full saturation, color chosen by palette + PartSys->sources[i].vx = 0; //emit speed in x + PartSys->sources[i].source.y = 10; + PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } - // update particles, create particles + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness + MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state - i = 0; - for (j = 0; j < numMeteors; j++) + + for (i = 0; i < numMeteors; i++) { // determine meteor state by its speed: - if (meteors[j].source.vy < 0) // moving down, emit sparks + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks { - #ifdef ESP8266 + #ifdef ESP8266 emitparticles = 1; - #else + #else emitparticles = 2; - #endif + #endif } - else if (meteors[j].source.vy > 0) // moving up means meteor is on 'standby' + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' { emitparticles = 0; } else // speed is zero, explode! { - meteors[j].source.vy = 125; // set source speed positive so it goes into timeout and launches again - #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 2) + 10; // defines the size of the explosion - #else + PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again + #ifdef ESP8266 + emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif - } - - while(i < numParticles) - { - if (particles[i].ttl == 0) // particle is dead - { - if (emitparticles > 0) - { - Emitter_Fountain_emit(&meteors[j], &particles[i]); - emitparticles--; - } - else - break; // done emitting for this meteor - } - i++; + #endif } - } - - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = fully hard, no energy is lost in collision - - if (SEGMENT.check3) // use collisions if option is set - { - detectCollisions(particles, numParticles, hardness); - } - // update particles - for (i = 0; i < numParticles; i++) - { - if (particles[i].ttl) + for (int e = emitparticles; e > 0; e--) { - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, true, hardness); + PartSys->sprayEmit(PartSys->sources[i]); } } // update the meteors, set the speed state for (i = 0; i < numMeteors; i++) { - if (meteors[i].source.ttl) + if (PartSys->sources[i].source.ttl) { - Particle_Gravity_update(&meteors[i].source, SEGMENT.check1, SEGMENT.check2, true, 255); // move the meteor, age the meteor (ttl--) - if (meteors[i].source.vy > 0) - meteors[i].source.y = 5; //'hack' to keep the meteors within frame, as ttl will be set to zero by gravity update if too far out of frame + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((meteors[i].source.y < PS_P_RADIUS) && (meteors[i].source.vy < 0)) // reached the bottom pixel on its way down + if ((PartSys->sources[i].source.y < PS_P_RADIUS<<1) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down { - meteors[i].source.vy = 0; // set speed zero so it will explode - meteors[i].source.vx = 0; - meteors[i].source.y = 5; // offset from ground so explosion happens not out of frame - meteors[i].source.collide = true; // explosion particles will collide if checked - meteors[i].maxLife = 200; - meteors[i].minLife = 50; + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) + PartSys->sources[i].source.collide = true; #ifdef ESP8266 - meteors[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + PartSys->sources[i].maxLife = 130; + PartSys->sources[i].minLife = 20; + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #else - meteors[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #endif - meteors[i].vx = 0; // emitting speed x - meteors[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - meteors[i].var = (SEGMENT.custom1 >> 1); // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].maxLife = 200; + PartSys->sources[i].minLife = 50; + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #endif + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } - } - else if (meteors[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + } + else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor - meteors[i].source.y = Max_y + (PS_P_RADIUS << 2); // start 4 pixels above the top - meteors[i].source.x = random16(Max_x); - meteors[i].source.vy = -random16(30) - 30; // meteor downward speed - meteors[i].source.vx = random16(30) - 15; - meteors[i].source.hue = random16(); // random color - meteors[i].source.ttl = 1000; // long life, will explode at bottom - meteors[i].source.collide = false; // trail particles will not collide - meteors[i].maxLife = 60; // spark particle life - meteors[i].minLife = 20; - meteors[i].vx = 0; // emitting speed - meteors[i].vy = -9; // emitting speed (down) - meteors[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = random16(PartSys->maxX); + PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = random16(30) - 15; + PartSys->sources[i].source.hue = random16(); // random color + PartSys->sources[i].source.ttl = 255; // long life, will explode at bottom + PartSys->sources[i].source.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) } } - SEGMENT.fill(BLACK); // clear the matrix - // render the particles - ParticleSys_render(particles, numParticles, false, false); + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;012;pal=0,sx=32,ix=85,c1=100,c2=100,c3=8,o1=0,o2=1,o3=1"; +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=120,c2=125,c3=8,o1=0,o2=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -9185,285 +8925,349 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); + ParticleSystem *PartSys = NULL; + uint32_t i = 0; + PSparticle *attractor; //particle pointer to the attractor + uint8_t *counters; // counters for the applied force + PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system box dimensions - const uint32_t Max_x(cols * PS_P_RADIUS - 1); - const uint32_t Max_y(rows * PS_P_RADIUS - 1); + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) + if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters + return mode_static(); // allocation failed; //allocation failed + + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.sat = 255; // set full saturation + PartSys->sources[0].source.x = PS_P_RADIUS; //start out in bottom left corner + PartSys->sources[0].source.y = PS_P_RADIUS<<1; + PartSys->sources[0].source.vx = random16(5) + 3; + PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; //move slower in y + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].vx = 0; // emitting speed + PartSys->sources[0].vy = 0; // emitting speed + PartSys->sources[0].var = 7; // emiting variation + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -#ifdef ESP8266 - const uint32_t numParticles = 90; // maximum number of particles -#else - const uint32_t numParticles = 300; // maximum number of particles -#endif + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(230); //walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); - PSparticle *particles; - PSparticle *attractor; - PSpointsource *spray; - uint8_t *counters; // counters for the applied force + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); + PartSys->setUsedParticles(displayparticles); + + // set pointers + attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); + counters = reinterpret_cast(PartSys->PSdataEnd); + //set attractor properties + if(SEGMENT.check2) //move attractor + { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } + else{ + attractor->vx = 0; // not moving + attractor->vy = 0; + attractor->x = PartSys->maxX >> 1; // center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) + { + PartSys->sources[0].source.hue++; + PartSys->sources[0].source.ttl = 100; //spray never dies + } - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * (numParticles+1); //space for particles and the attractor - dataSize += sizeof(uint8_t) * numParticles; //space for counters - dataSize += sizeof(PSpointsource); //space for spray + SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + else + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed - // divide and cast the data array into correct pointers - particles = reinterpret_cast(SEGENV.data); - attractor = reinterpret_cast(particles + numParticles + 1); - spray = reinterpret_cast(attractor + 1); - counters = reinterpret_cast(spray + 1); + // apply force + for(i = 0; i < displayparticles; i++) + { + PartSys->attract(&PartSys->particles[i], attractor, counters, SEGMENT.speed, SEGMENT.check3); + } + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); - uint32_t i; + PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; - if (SEGMENT.call == 0) // initialization - { - attractor->vx = 0; - attractor->vy = 0; - attractor->x = Max_x >> 1; // center - attractor->y = Max_y >> 1; +/* +Particle Spray, just a particle spray with many parameters +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - - spray->source.hue = random16(); - spray->source.sat = 255; //full saturation, color by palette - spray->source.x = 0; - spray->source.y = 0; - spray->source.vx = random16(5) + 2; - spray->source.vy = random16(4) + 1; - spray->source.ttl = 100; - spray->source.collide = true; // seeded particles will collide (if checked) - spray->maxLife = 300; // seeded particle lifetime in frames - spray->minLife = 30; - spray->vx = 0; // emitting speed - spray->vy = 0; // emitting speed - spray->var = 6; // emitting speed variation - } - - uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 1, numParticles) - 2; //TODO: the -2 is a botch fix, it crashes for some reason if going to max number of particles, is this a rounding error? - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - i = 0; +uint16_t mode_particlespray(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + uint8_t numSprays; + uint32_t i; + const uint8_t hardness = 200; //collision hardness is fixed - if (hardness > 1) // enable collisions + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - detectCollisions(particles, displayparticles, hardness); + if (!initParticleSystem(PartSys)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 100; + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].var = 7; + } } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - if (SEGMENT.call % 5 == 0) + if (PartSys == NULL) { - spray->source.hue++; - spray->source.ttl = 100; // spray never dies + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } - uint8_t emit = 1; // number of particles emitted per frame - Particle_Bounce_update(&spray->source, 255); // bounce the spray around + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->enableGravity(SEGMENT.check1); + numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); - SEGMENT.aux0++; // emitting angle + uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - // now move the particles - for (i = 0; i < displayparticles; i++) + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { - - if (particles[i].ttl == 0 && emit--) // find a dead particle + for (i = 0; i < numSprays; i++) { - if (SEGMENT.call % 2 == 0) // alternate direction of emit - Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0, SEGMENT.custom1 >> 4); - else - Emitter_Angle_emit(spray, &particles[i], SEGMENT.aux0 + 128, SEGMENT.custom1 >> 4); // emit at 180° as well + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); } - // every now and then, apply 'air friction' to smooth things out, slows down all particles a little - if (SEGMENT.custom3 > 0) + uint8_t j = 0; + for (i = 0; i < percycle; i++) { - if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) - { - applyFriction(&particles[i], 4); - } + // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[j], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); + j = (j + 1) % numSprays; } - - Particle_attractor(&particles[i], attractor, &counters[i], SEGMENT.speed, SEGMENT.check3); - if (SEGMENT.check1) - Particle_Bounce_update(&particles[i], hardness); - else - Particle_Move_update(&particles[i]); } - if (SEGMENT.check2) - SEGMENT.fadeToBlackBy(20); // fade the matrix - else - SEGMENT.fill(BLACK); // clear the matrix - - // ParticleSys_render(&attract, 1, 30, false, false); // render attractor - // render the particles - ParticleSys_render(particles, displayparticles, false, false); - + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Bounce,Trails,Swallow;;!;012;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + /* -Particle Spray, just a simple spray animation with many parameters +Particle base Graphical Equalizer Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particlespray(void) +uint16_t mode_particleGEQ(void) { - if (SEGLEN == 1) return mode_static(); - const uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - // particle system x dimension - const uint32_t Max_x = (cols * PS_P_RADIUS - 1); - const uint32_t Max_y = (rows * PS_P_RADIUS - 1); - -#ifdef ESP8266 - const uint32_t numParticles = 80; -#else - const uint32_t numParticles = 450; -#endif - - const uint8_t numSprays = 1; - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - - PSparticle *particles; - PSpointsource *spray; + ParticleSystem *PartSys = NULL; - // allocate memory and divide it into proper pointers, max is 32k for all segments. - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSpointsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys)) // init + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS - spray = reinterpret_cast(SEGENV.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + if (PartSys == NULL) + { + Serial.println("ERROR: paticle system not found, nullpointer"); + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + } - uint32_t i = 0; - uint32_t j = 0; + uint32_t i; + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->enableGravity(true, SEGMENT.custom3<<1); //set gravity strength - if (SEGMENT.call == 0) // initialization + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - for (i = 0; i < numParticles; i++) - { - particles[i].ttl = 0; - } - for (i = 0; i < numSprays; i++) - { - spray[i].source.hue = random16(); - spray[i].source.sat = 255; // set full saturation - spray[i].source.x = (cols * PS_P_RADIUS) / (numSprays + 1) * (i + 1); - spray[i].source.y = 5; // just above the lower edge, if zero, particles already 'bounce' at start and loose speed. - spray[i].source.vx = 0; - spray[i].maxLife = 300; // lifetime in frames - spray[i].minLife = 20; - spray[i].source.collide = true; // seeded particles will collide - spray[i].vx = 0; // emitting speed - spray[i].vy = 0; // emitting speed - spray[i].var = 10; - } + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); } - // change source emitting color from time to time - if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 + //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? + //implement it simply first, then add complexity... need to check what looks good + i = 0; + uint32_t bin; //current bin + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (bin = 0; bin < 16; bin++) { - for (i = 0; i < numSprays; i++) + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9); // emit speed according to loudness of band + emitparticles = 0; + + if (fftResult[bin] > threshold) { - spray[i].source.hue++; // = random16(); //change hue of spray source - // spray[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - spray[i].source.x = map(SEGMENT.custom1, 0, 255, 0, Max_x); - spray[i].source.y = map(SEGMENT.custom2, 0, 255, 0, Max_y); + emitparticles = 1;// + (fftResult[bin]>>6); } - - i = 0; - j = 0; - for (i = 0; i < numParticles; i++) + else if(fftResult[bin] > 0)// band has low volue { - if (particles[i].ttl == 0) // find a dead particle + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (random16() % restvolume == 0) { - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - Emitter_Angle_emit(&spray[j], &particles[i], 255-(SEGMENT.custom3 << 3), SEGMENT.speed >> 2); - j = (j + 1) % numSprays; - if (percycle-- == 0) - { - break; // quit loop if all particles of this round emitted - } + emitparticles = 1; } } - } - - const uint8_t hardness = 200; - if (SEGMENT.check3) // collisions enabled - detectCollisions(particles, numParticles, hardness); - - for (i = 0; i < numParticles; i++) - { - // particles[i].hue = min((uint16_t)220, particles[i].ttl); - if (SEGMENT.check1) // use gravity - Particle_Gravity_update(&particles[i], SEGMENT.check2, SEGMENT.check2 == 0, true, hardness); - else // bounce particles + while (i < PartSys->usedParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority { - if (SEGMENT.check2) // wrap x - Particle_Move_update(&particles[i], true, true, false); - else // bounce - Particle_Bounce_update(&particles[i], hardness); + if (PartSys->particles[i].ttl == 0) // find a dead particle + { + //set particle properties + PartSys->particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width + PartSys->particles[i].y = 0; //start at the bottom + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].sat = 255; // set saturation + emitparticles--; + } + i++; } } - SEGMENT.fill(BLACK); // clear the matrix - - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check2, false); - + PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;012;pal=0,sx=150,ix=90,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; -/* -Particle base Graphical Equalizer -Uses palette for particle color -by DedeHai (Damian Schneider) -*/ -uint16_t mode_particleGEQ(void) +/* + * Particle rotating GEQ + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ +/* +uint16_t mode_particlecenterGEQ(void) { if (SEGLEN == 1) return mode_static(); const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - // const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - const uint32_t numParticles = 150; // maximum number of particles + const uint32_t numParticles = 50; // maximum number of particles #else const uint32_t numParticles = 500; // maximum number of particles #endif + const uint8_t numSprays = 16; // maximum number of sprays + PSparticle *particles; + PSsource *spray; - // allocate memory and divide it into proper pointers, max is 32k for all segments. + // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes uint32_t dataSize = sizeof(PSparticle) * numParticles; + dataSize += sizeof(PSsource) * (numSprays); if (!SEGENV.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed + spray = reinterpret_cast(SEGENV.data); // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(SEGENV.data); // cast the data array into a particle pointer - uint32_t i; + particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer + + uint32_t i = 0; + uint32_t j = 0; + //uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 + if (SEGMENT.call == 0) // initialization { + SEGMENT.aux0 = 0; // starting angle + SEGMENT.aux1 = 0xFF; // user check for (i = 0; i < numParticles; i++) { - particles[i].ttl = 0; - particles[i].sat = 255; // full color + PartSys->particles[i].ttl = 0; + } + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.hue = i*16; //even color distribution + PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center + PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].source.vy = 0; + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].vy = 0; // emitting speed + PartSys->sources[i].var = 0; // emitting variation } } @@ -9476,72 +9280,63 @@ uint16_t mode_particleGEQ(void) uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 - // map the bands into 16 positions on x axis, emit some particles according to frequency loudness - // Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 - // in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? - // implement it simply first, then add complexity... need to check what looks good i = 0; - uint32_t bin; // current bin - uint32_t binwidth = (cols * PS_P_RADIUS - 1) >> 4; // emit poisition variation for one bin (+/-) + uint32_t threshold = 300 - SEGMENT.intensity; - uint32_t emitparticles = 0; - for (bin = 0; bin < 16; bin++) - { - uint32_t xposition = binwidth * bin + (binwidth >> 1); // emit position according to frequency band - uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9); // emit speed according to loudness of band - emitparticles = 0; + i = 0; + j = random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.custom1; + else + SEGMENT.aux0 -= SEGMENT.custom1; - if (fftResult[bin] > threshold) - { - emitparticles = 1; // + (fftResult[bin]>>6); - } - else if (fftResult[bin] > 0) // band has low volue + uint32_t angleoffset = SEGMENT.aux0 >> 4; + + while (i < numParticles) { - uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; - if (random16() % restvolume == 0) + if (PartSys->particles[i].ttl == 0) // find a dead particle { - emitparticles = 1; - } - } + uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band + uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; - while (i < numParticles && emitparticles > 0) // emit particles if there are any left, low frequencies take priority - { - if (particles[i].ttl == 0) // find a dead particle - { - //set particle properties - particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - particles[i].y = 0; //start at the bottom - particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation - particles[i].vy = emitspeed; - particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - //particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - emitparticles--; + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + { + emitparticles = 1; // + (fftResult[bin]>>6); + } + else if (fftResult[j] > 0) // band has low volue + { + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (random16() % restvolume == 0) + { + emitparticles = 1; + } + } + if (emitparticles) + Emitter_Angle_emit(&spray[j], &PartSys->particles[i], emitangle, emitspeed); + j = (j + 1) % numSprays; } i++; + //todo: could add a break if all 16 sprays have been checked, would speed it up } - } - uint8_t hardness = SEGMENT.custom2; // how hard the collisions are, 255 = full hard. - // detectCollisions(particles, numParticles, hardness); - // now move the particles - for (i = 0; i < numParticles; i++) - { - particles[i].vy -= (SEGMENT.custom3>>3); // apply stronger gravity - Particle_Gravity_update(&particles[i], SEGMENT.check1, SEGMENT.check2, SEGMENT.check3, hardness); - } - SEGMENT.fill(BLACK); // clear the matrix + for (i = 0; i < numParticles; i++) + { + Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles + } + + + // render the particles + ParticleSys_render(particles, numParticles, false, false); - // render the particles - ParticleSys_render(particles, numParticles, SEGMENT.check1, false); // custom3 slider is saturation return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,WrapX,BounceX,Floor;;!;012;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; - + static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; +*/ #endif // WLED_DISABLE_2D @@ -9783,18 +9578,20 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); - addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); - addEffect(FX_MODE_PARTICLEROTATINGSPRAY, &mode_particlerotatingspray, _data_FX_MODE_PARTICLEROTATINGSPRAY); - addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); - addEffect(FX_MODE_PARTICLEFALL, &mode_particlefall, _data_FX_MODE_PARTICLEFALL); - addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); - addEffect(FX_MODE_PARTICLEGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + + // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index f796ffe3cb..bfdee58173 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -321,16 +321,17 @@ #define FX_MODE_PARTICLEVOLCANO 187 #define FX_MODE_PARTICLEFIRE 188 #define FX_MODE_PARTICLEFIREWORKS 189 -#define FX_MODE_PARTICLEROTATINGSPRAY 190 +#define FX_MODE_PARTICLEVORTEX 190 #define FX_MODE_PARTICLEPERLIN 191 -#define FX_MODE_PARTICLEFALL 192 +#define FX_MODE_PARTICLEPIT 192 #define FX_MODE_PARTICLEBOX 193 #define FX_MODE_PARTICLEATTRACTOR 194 #define FX_MODE_PARTICLEIMPACT 195 #define FX_MODE_PARTICLEWATERFALL 196 #define FX_MODE_PARTICLESPRAY 197 -#define FX_MODE_PARTICLEGEQ 198 -#define MODE_COUNT 199 +#define FX_MODE_PARTICLESGEQ 198 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define MODE_COUNT 200 typedef enum mapping1D2D { M12_Pixels = 0, @@ -821,7 +822,7 @@ class WS2812FX { // 96 bytes inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId(void) { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId(void) { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count + inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint8_t getTargetFps() { return _targetFps; } // returns rough FPS value for las 2s interval inline uint8_t getModeCount() { return _modeCount; } // returns number of registered modes/effects diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index eb1fa972e6..1cd99e41c0 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -3,7 +3,6 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys LICENSE The MIT License (MIT) @@ -32,702 +31,833 @@ this should be used to optimize speed but not if memory is affected much */ +/* + TODO: + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. + -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not + -add an x/y struct, do particle rendering using that, much easier to read + -extend rendering to more than 2x2, 3x2 (fire) should be easy, 3x3 maybe also doable without using much math (need to see if it looks good) + +*/ +// sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" #include "FX.h" -// Fountain style emitter for particles used for flames (particle TTL depends on source TTL) -void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) { - part->x = emitter->source.x + random16(PS_P_RADIUS) - PS_P_HALFRADIUS; // jitter the flame by one pixel to make the flames wider and softer - part->y = emitter->source.y; - part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); - part->ttl = (uint8_t)((rand() % (emitter->maxLife - emitter->minLife)) + emitter->minLife + emitter->source.ttl); // flame intensity dies down with emitter TTL - // part->hue = emitter->source.hue; //fire uses ttl and not hue for heat - // part->sat = emitter->source.sat; //flame does not use saturation + Serial.println("PS Constructor"); + numSources = numberofsources; + numParticles = numberofparticles; // set number of particles in the array + usedParticles = numberofparticles; // use all particles by default + //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default + updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + setMatrixSize(width, height); + setWallHardness(255); // set default wall hardness to max + emitIndex = 0; + /* + Serial.println("alive particles: "); + for (int i = 0; i < numParticles; i++) + { + //particles[i].ttl = 0; //initialize all particles to dead + //if (particles[i].ttl) + { + Serial.print("x:"); + Serial.print(particles[i].x); + Serial.print(" y:"); + Serial.println(particles[i].y); + } + }*/ + Serial.println("PS Constructor done"); } -// fountain style emitter -void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part) +//update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem::update(void) { - part->x = emitter->source.x; // + random16(emitter->var) - (emitter->var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. - part->y = emitter->source.y; // + random16(emitter->var) - (emitter->var >> 1); - part->vx = emitter->vx + random16(emitter->var) - (emitter->var >> 1); - part->vy = emitter->vy + random16(emitter->var) - (emitter->var >> 1); - part->ttl = random16(emitter->maxLife - emitter->minLife) + emitter->minLife; - part->hue = emitter->source.hue; - part->sat = emitter->source.sat; - part->collide = emitter->source.collide; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(particles, usedParticles, gforce, &gforcecounter); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (int i = 0; i < usedParticles; i++) + { + particleMoveUpdate(particles[i], particlesettings); + } + + ParticleSys_render(); } -// Emits a particle at given angle and speed, angle is from 0-255 (=0-360deg), speed is also affected by emitter->var -void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed) +//update function for fire animation +void ParticleSystem::updateFire(uint32_t intensity) { - emitter->vx = (((int16_t)cos8(angle) - 127) * speed) >> 7; // cos is signed 8bit, so 1 is 127, -1 is -127, shift by 7 - emitter->vy = (((int16_t)sin8(angle) - 127) * speed) >> 7; - Emitter_Fountain_emit(emitter, part); + fireParticleupdate(); + ParticleSys_render(true, intensity); } -// attracts a particle to an attractor particle using the inverse square-law -void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) + +void ParticleSystem::setUsedParticles(uint16_t num) { - // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - particle->x; - int32_t dy = attractor->y - particle->y; + usedParticles = min(num, numParticles); //limit to max particles +} - // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy + 1; - if (distanceSquared < 4096) - { - if (swallow) // particle is close, kill it - { - particle->ttl = 0; - return; - } - distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance of particle size to avoid very high forces - } +void ParticleSystem::setWallHardness(uint8_t hardness) +{ + wallHardness = hardness; +} - int32_t shiftedstrength = (int32_t)strength << 16; - int32_t force; - int32_t xforce; - int32_t yforce; - int32_t xforce_abs; // absolute value - int32_t yforce_abs; +void ParticleSystem::setCollisionHardness(uint8_t hardness) +{ + collisionHardness = hardness; +} - force = shiftedstrength / distanceSquared; - xforce = (force * dx) >> 10; // scale to a lower value, found by experimenting - yforce = (force * dy) >> 10; - xforce_abs = abs(xforce); // absolute value - yforce_abs = abs(yforce); +void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) +{ + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions +} - uint8_t xcounter = (*counter) & 0x0F; // lower four bits - uint8_t ycounter = (*counter) >> 4; // upper four bits +void ParticleSystem::setWrapX(bool enable) +{ + particlesettings.wrapX = enable; +} - *counter = 0; // reset counter, is set back to correct values below +void ParticleSystem::setWrapY(bool enable) +{ + particlesettings.wrapY = enable; +} - // for small forces, need to use a delay timer (counter) - if (xforce_abs < 16) - { - xcounter += xforce_abs; - if (xcounter > 15) - { - xcounter -= 15; - *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits - // apply force in x direction - if (dx < 0) - { - particle->vx -= 1; - } - else - { - particle->vx += 1; - } - } - else // save counter value - *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits - } - else - { - particle->vx += xforce >> 4; // divide by 16 - } +void ParticleSystem::setBounceX(bool enable) +{ + particlesettings.bounceX = enable; +} - if (yforce_abs < 16) - { - ycounter += yforce_abs; +void ParticleSystem::setBounceY(bool enable) +{ + particlesettings.bounceY = enable; +} - if (ycounter > 15) - { - ycounter -= 15; - *counter |= (ycounter << 4) & 0xF0; // write upper four bits +void ParticleSystem::setKillOutOfBounds(bool enable) +{ + particlesettings.killoutofbounds = enable; +} - if (dy < 0) - { - particle->vy -= 1; - } - else - { - particle->vy += 1; - } - } - else // save counter value - *counter |= (ycounter << 4) & 0xF0; // write upper four bits - } - else - { - particle->vy += yforce >> 4; // divide by 16 - } - // TODO: need to limit the max speed? +void ParticleSystem::setColorByAge(bool enable) +{ + particlesettings.colorByAge = enable; } -// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 -void Particle_Move_update(PSparticle *part, bool killoutofbounds, bool wrapX, bool wrapY) +// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +void ParticleSystem::enableGravity(bool enable, uint8_t force) { - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + particlesettings.useGravity = enable; + if (force > 0) + gforce = force; + else + particlesettings.useGravity = false; +} - // particle box dimensions - const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; - const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; +void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +{ + particlesettings.useCollisions = enable; + collisionHardness = hardness + 1; +} - if (part->ttl > 0) +// emit one particle with variation +void ParticleSystem::sprayEmit(PSsource &emitter) +{ + for (uint32_t i = 0; i < usedParticles; i++) { - // age - part->ttl--; - - // apply velocity - int32_t newX, newY; - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; - - part->outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - // x direction, handle wraparound - if (wrapX) + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle { - newX = newX % (PS_MAX_X + 1); - if (newX < 0) - newX = PS_MAX_X - newX; + particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. + particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); + particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); + particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; + break; } - else if ((part->x <= 0) || (part->x >= PS_MAX_X)) // check if particle is out of bounds + /* + if (emitIndex < 2) { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->x = newX; // set new position - - if (wrapY) - { - newY = newY % (PS_MAX_Y + 1); - if (newY < 0) - newY = PS_MAX_Y - newY; - } - else if ((part->y <= 0) || (part->y >= PS_MAX_Y)) // check if particle is out of bounds - { - if (killoutofbounds) - part->ttl = 0; - else - part->outofbounds = 1; - } - part->y = newY; // set new position + Serial.print(" "); + Serial.print(particles[emitIndex].ttl); + Serial.print(" "); + Serial.print(particles[emitIndex].x); + Serial.print(" "); + Serial.print(particles[emitIndex].y); + }*/ } + //Serial.println("**"); } -// bounces a particle on the matrix edges, if surface 'hardness' is <255 some energy will be lost in collision (127 means 50% lost) -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness) +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem::flameEmit(PSsource &emitter) { - // Matrix dimension - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle box dimensions - const uint16_t PS_MAX_X(cols * PS_P_RADIUS - 1); - const uint16_t PS_MAX_Y(rows * PS_P_RADIUS - 1); - - if (part->ttl > 0) + for (uint32_t i = 0; i < usedParticles; i++) { - // age - part->ttl--; - - part->outofbounds = 0; // reset out of bounds (particles are never out of bounds) - - // apply velocity - int16_t newX, newY; - - // apply velocity - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; - - if ((newX <= 0) || (newX >= PS_MAX_X)) - { // reached an edge - part->vx = -part->vx; // invert speed - part->vx = (((int16_t)part->vx) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface - } - - if ((newY <= 0) || (newY >= PS_MAX_Y)) - { // reached an edge - part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * ((int16_t)hardness + 1)) >> 8; // reduce speed as energy is lost on non-hard surface + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); //random16 is good enough for fire and much faster + particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife + emitter.source.ttl; // flame intensity dies down with emitter TTL + // fire uses ttl and not hue for heat, so no need to set the hue + break; // done } - - newX = max(newX, (int16_t)0); // limit to positive - newY = max(newY, (int16_t)0); - part->x = min(newX, (int16_t)PS_MAX_X); // limit to matrix boundaries - part->y = min(newY, (int16_t)PS_MAX_Y); } } -// particle moves, gravity force is applied and ages, if wrapX is set, pixels leaving in x direction reappear on other side, hardness is surface hardness for bouncing (127 means 50% speed lost each bounce) -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness) -{ - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +//todo: idee: man könnte einen emitter machen, wo die anzahl emittierten partikel von seinem alter abhängt. benötigt aber einen counter +//idee2: source einen counter hinzufügen, dann setting für emitstärke, dann müsste man das nicht immer in den FX animationen handeln - // particle box dimensions - const int32_t PS_MAX_X = cols * PS_P_RADIUS - 1; - const int32_t PS_MAX_Y = rows * PS_P_RADIUS - 1; +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed) +{ + emitter.vx = ((int32_t)cos16(angle) * speed) / 32767; // cos16() and sin16() return signed 16bit + emitter.vy = ((int32_t)sin16(angle) * speed) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + sprayEmit(emitter); +} - if (part->ttl > 0) +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) +{ + if (part.ttl > 0) { // age - part->ttl--; + part.ttl--; + if (particlesettings.colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - // check if particle is out of bounds or died - if ((part->y < -PS_P_RADIUS) || (part->y >= PS_MAX_Y << 2)) - { // if it moves more than 1 pixel below y=0, it will not come back. also remove particles that too far above - part->ttl = 0; - return; // particle died, we are done - } - if (wrapX == false) + int32_t newX = part.x + (int16_t)part.vx; + int32_t newY = part.y + (int16_t)part.vy; + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew + if (options.bounceX) { - if ((part->x < -PS_MAX_X) || (part->x >= PS_MAX_X << 2)) - { // left and right: keep it alive as long as its not too far out (if adding more effects like wind, it may come back) - part->ttl = 0; - return; // particle died, we are done + if ((newX < PS_P_RADIUS) || (newX > maxX - PS_P_RADIUS)) // reached a wall + { + part.vx = -part.vx; // invert speed + part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < PS_P_RADIUS) + newX = PS_P_RADIUS; // fast particles will never reach the edge if position is inverted + else + newX = maxX - PS_P_RADIUS; + } + } + + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge + { + if (options.wrapX) + { + newX = wraparound(newX, maxX); + } + else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + part.outofbounds = 1; + if (options.killoutofbounds) + part.ttl = 0; } } - // apply acceleration (gravity) every other frame, doing it every frame is too strong - if (SEGMENT.call % 2 == 0) + if (options.bounceY) { - if (part->vy > -MAXGRAVITYSPEED) - part->vy = part->vy - 1; + if ((newY < PS_P_RADIUS) || (newY > maxY - PS_P_RADIUS)) // reached floor / ceiling + { + if (newY < PS_P_RADIUS) // bounce at bottom + { + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + newY = PS_P_RADIUS; + } + else + { + if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX + { + if (newY > maxY + PS_P_HALFRADIUS) + part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) + } + else + { + part.vy = -part.vy; // invert speed + part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + newY = maxY - PS_P_RADIUS; + } + } + } + } + + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge + { + if (options.wrapY) + { + newY = wraparound(newY, maxY); + } + else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + part.outofbounds = 1; + if (options.killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options.useGravity) + part.ttl = 0; + } + } + } + + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } +} - // apply velocity - int32_t newX, newY; +// apply a force in x,y direction to particles +// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter) +{ + // for small forces, need to use a delay counter + uint8_t xcounter = (*counter) & 0x0F; // lower four bits + uint8_t ycounter = (*counter) >> 4; // upper four bits - newX = part->x + (int16_t)part->vx; - newY = part->y + (int16_t)part->vy; + // velocity increase + int32_t dvx = calcForce_dV(xforce, &xcounter); + int32_t dvy = calcForce_dV(yforce, &ycounter); - part->outofbounds = 0; - // check if particle is outside of displayable matrix + // save counter values back + *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + *counter |= (ycounter << 4) & 0xF0; // write upper four bits - // x direction, handle wraparound (will overrule bounce x) and bounceX - if (wrapX) + // apply the force to particle: + int32_t i = 0; + if (dvx != 0) + { + if (numparticles == 1) // for single particle, skip the for loop to make it faster { - newX = newX % (PS_MAX_X + 1); - if (newX < 0) - newX = PS_MAX_X - newX; + part[0].vx = part[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vx + dvx; // limit the force, this is faster than min or if/else } else { - if (newX < 0 || newX > PS_MAX_X) - { // reached an edge - if (bounceX) - { - part->vx = -part->vx; // invert speed - part->vx = (((int16_t)part->vx) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newX = max(newX, (int32_t)0); // limit to positive - newX = min(newX, (int32_t)PS_MAX_X); // limit to matrix boundaries - } - else // not bouncing and out of matrix - part->outofbounds = 1; + for (i = 0; i < numparticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty + part[i].vx = part[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + dvx; } } - - part->x = newX; // set new position - - // y direction, handle bounceY (bounces at ground only) - if (newY < 0) - { // || newY > PS_MAX_Y) { //reached an edge - if (bounceY) + } + if (dvy != 0) + { + if (numparticles == 1) // for single particle, skip the for loop to make it faster + part[0].vy = part[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vy + dvy; + else + { + for (i = 0; i < numparticles; i++) { - part->vy = -part->vy; // invert speed - part->vy = (((int16_t)part->vy) * (int16_t)hardness) >> 8; // reduce speed as energy is lost on non-hard surface - newY = max(newY, (int32_t)0); // limit to positive (helps with piling as that can push particles out of frame) - // newY = min(newY, (int16_t)PS_MAX_Y); //limit to matrix boundaries + part[i].vy = part[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + dvy; } - else // not bouncing and out of matrix - part->outofbounds = 1; } - - part->y = newY; // set new position } } -// render particles to the LED buffer (uses palette to render the 8bit particle color value) -// if wrap is set, particles half out of bounds are rendered to the other side of the matrix -void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY) +// apply a force in x,y direction to particles directly (no counter required but no 'sub 1' force supported) +void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce) { -#ifdef ESP8266 - bool fastcoloradd = true; // on ESP8266, we need every bit of performance we can get -#else - bool fastcoloradd = false; // on ESP32, there is little benefit from using fast add -#endif + //note: could make this faster for single particles by adding an if statement, but it is fast enough as is + for (uint i = 0; i < numparticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty + part[i].vx = part[i].vx + xforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + xforce; + part[i].vy = part[i].vy + yforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + yforce; + } +} - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +// apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code +// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter) +{ + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) + applyForce(part, numparticles, xforce, yforce, counter); +} - int32_t x, y; - uint32_t dx, dy; - uint32_t intensity; - CRGB baseRGB; - uint32_t i; - uint32_t brightess; // particle brightness, fades if dying +// apply a force in angular direction to particles directly (no counter required but no 'sub 1' force supported) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle) +{ + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(part, numparticles, xforce, yforce); +} - uint32_t precal1, precal2, precal3; // precalculate values to improve speed +// apply gravity to a group of particles +// faster than apply force since direction is always down and counter is fixed for all particles +// caller needs to provide a 8bit counter that holds its value between calls +// force is in 4.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results), force above 127 are VERY strong +void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter) +{ + int32_t dv; // velocity increase + if (force > 15) + dv = (force >> 4); // apply the 4 MSBs + else + dv = 1; + *counter += force; - // go over particles and update matrix cells on the way - for (i = 0; i < numParticles; i++) + if (*counter > 15) { - if (particles[i].ttl == 0 || particles[i].outofbounds) - { - continue; - } - // generate RGB values for particle - brightess = min(particles[i].ttl, (uint16_t)255); - - if (particles[i].sat < 255) + *counter -= 16; + // apply force to all used particles + for (uint32_t i = 0; i < numarticles; i++) { - CHSV baseHSV = rgb2hsv_approximate(ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND)); - baseHSV.s = particles[i].sat; - baseRGB = (CRGB)baseHSV; + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = particles[i].vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vy - dv; // limit the force, this is faster than min or if/else } - else - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + } +} - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; - dx = xoffset % (uint32_t)PS_P_RADIUS; - dy = yoffset % (uint32_t)PS_P_RADIUS; - x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (yoffset) >> PS_P_RADIUS_SHIFT; - - // calculate brightness values for all six pixels representing a particle using linear interpolation - // precalculate values for speed optimization - precal1 = PS_P_RADIUS - dx; - precal2 = (PS_P_RADIUS - dy) * brightess; // multiply by ttl, adds more heat for younger particles - precal3 = dy * brightess; - - if (wrapX) - { // wrap it to the other side if required - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; - } - } - if (wrapY) - { // wrap it to the other side if required - if (y < 0) - { // left half of particle render is out of frame, wrap it - y = rows - 1; - } - } +//apply gravity using PS global gforce +void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter) +{ + applyGravity(part, numarticles, gforce, counter); +} - // calculate brightness values for all four pixels representing a particle using linear interpolation, - // add color to the LEDs. - // intensity is a scaling value from 0-255 (0-100%) +//apply gravity to single particle using system settings (use this for sources) +void ParticleSystem::applyGravity(PSparticle *part) +{ + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; - // bottom left - // calculate the intensity with linear interpolation, divide by surface area (shift by PS_P_SURFACE) to distribute the energy - intensity = (precal1 * precal2) >> PS_P_SURFACE; // equal to (PS_P_RADIUS - dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - // scale the particle base color by the intensity and add it to the pixel - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - - // bottom right; - x++; - if (wrapX) - { // wrap it to the other side if required - if (x >= cols) - x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows) - { - intensity = (dx * precal2) >> PS_P_SURFACE; // equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } - // top right - y++; - if (wrapY) - { // wrap it to the other side if required - if (y >= rows) - y = y % rows; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows) - { - intensity = (dx * precal3) >> PS_P_SURFACE; // equal to (dx * dy * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } - // top left - x--; - if (wrapX) - { // wrap it to the other side if required - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; - } - } - if (x < cols && y < rows) - { - intensity = (precal1 * precal3) >> PS_P_SURFACE; // equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - SEGMENT.addPixelColorXY(x, rows - y - 1, baseRGB.scale8(intensity), fastcoloradd); - } + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else } } -// update & move particle, wraps around left/right if wrapX is true, wrap around up/down if wrapY is true -// particles move upwards faster if ttl is high (i.e. they are hotter) -void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX) +// slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) { - // Matrix dimension - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - // particle box dimensions - const int32_t PS_MAX_X = (cols * (uint32_t)PS_P_RADIUS - 1); - const int32_t PS_MAX_Y = (rows * (uint32_t)PS_P_RADIUS - 1); - uint32_t i = 0; + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + part->vx = ((int16_t)part->vx * friction) / 255; + part->vy = ((int16_t)part->vy * friction) / 255; +} - for (i = 0; i < numparticles; i++) +// apply friction to all particles +void ParticleSystem::applyFriction(uint8_t coefficient) +{ + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { - if (part[i].ttl > 0) - { - // age - part[i].ttl--; + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int16_t)particles[i].vy * friction) / 255; + } +} - // apply velocity - part[i].x = part[i].x + (int32_t)part[i].vx; - part[i].y = part[i].y + (int32_t)part[i].vy + (part[i].ttl >> 4); // younger particles move faster upward as they are hotter, used for fire +// attracts a particle to an attractor particle using the inverse square-law +void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +{ + // Calculate the distance between the particle and the attractor + int32_t dx = attractor->x - part->x; + int32_t dy = attractor->y - part->y; - part[i].outofbounds = 0; - // check if particle is out of bounds, wrap x around to other side if wrapping is enabled - // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve animation speed, only check x direction if y is not out of bounds - // y-direction - if (part[i].y < 0) - { - part[i].outofbounds = 1; - } - else if (part[i].y > PS_MAX_Y) // particle moved out on the top - { - part[i].ttl = 0; - } - else // particle is in frame in y direction, also check x direction now + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy + 1; + if (distanceSquared < 8192) + { + if (swallow) // particle is close, age it fast so it fades out, do not attract further + { + if (part->ttl > 7) + part->ttl -= 8; + else { - if ((part[i].x < 0) || (part[i].x > PS_MAX_X)) - { - if (wrapX) - { - part[i].x = part[i].x % (PS_MAX_X + 1); - if (part[i].x < 0) - part[i].x = PS_MAX_X - part[i].x; - } - else - { - part[i].ttl = 0; - } - } + part->ttl = 0; + return; } } + distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces } -} -// render fire particles to the LED buffer using heat to color -// each particle adds heat according to its 'age' (ttl) which is then rendered to a fire color in the 'add heat' function -void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX) -{ + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - const int32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const int32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + applyForce(part, 1, xforce, yforce, counter); +} - int32_t x, y; - uint32_t dx, dy; - uint32_t pixelheat; - uint32_t precal1, precal2, precal3; // precalculated values to improve speed +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +// fireintensity and firemode are optional arguments (fireintensity is only used in firemode) +void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) +{ + int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + CRGB baseRGB; + bool useLocalBuffer = true; + CRGB **colorbuffer; uint32_t i; - // go over particles and update matrix cells on the way - // note: some pixels (the x+1 ones) can be out of bounds, it is probably faster than to check that for every pixel as this only happens on the right border (and nothing bad happens as this is checked down the road) - for (i = 0; i < numParticles; i++) + uint32_t brightness; // particle brightness, fades if dying + //CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) + if (useLocalBuffer) { + // allocate memory for the local renderbuffer + colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); + if (colorbuffer == NULL) + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } - if (particles[i].outofbounds) - { + // go over particles and render them to the buffer + for (i = 0; i < usedParticles; i++) + { + if (particles[i].outofbounds || particles[i].ttl == 0) continue; - } - if (particles[i].ttl == 0) + // generate RGB values for particle + if(firemode) { - continue; + //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good + //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky + brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! + brightness > 255 ? 255 : brightness; // faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); } - - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[i].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[i].y - PS_P_HALFRADIUS; - dx = xoffset % (uint32_t)PS_P_RADIUS; - dy = yoffset % (uint32_t)PS_P_RADIUS; - x = (xoffset) >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - y = (yoffset) >> PS_P_RADIUS_SHIFT; - - if (wrapX) - { - if (x < 0) - { // left half of particle render is out of frame, wrap it - x = cols - 1; + else{ + brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + if (particles[i].sat < 255) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv + baseHSV.s = particles[i].sat; //desaturate + baseRGB = (CRGB)baseHSV; //convert back to RGB } } + int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there - // calculate brightness values for all six pixels representing a particle using linear interpolation - // precalculate values for speed optimization - precal1 = PS_P_RADIUS - dx; - precal2 = (PS_P_RADIUS - dy) * particles[i].ttl; //multiply by ttl, adds more heat for younger particles - precal3 = dy * particles[i].ttl; - - // bottom left - if (x < cols && x >= 0 && y < rows && y >= 0) + // calculate brightness values for all four pixels representing a particle using linear interpolation and calculate the coordinates of the phyiscal pixels to add the color to + renderParticle(&particles[i], brightness, pxlbrightness, pixco); + /* + //debug: check coordinates if out of buffer boundaries print out some info + for(uint32_t d; d<4; d++) { - pixelheat = (precal1 * precal2) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } - // bottom right; - x++; - if (wrapX) - { // wrap it to the other side if required - if (x >= cols) // if statement is faster on ESP8266 TODO: add a define - x = x % cols; // in case the right half of particle render is out of frame, wrap it (note: on microcontrollers with hardware division, the if statement is not really needed) - } - if (x < cols && y < rows && y >= 0) + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) + { + pxlbrightness[d] = -1; //do not render + Serial.print("uncought out of bounds: x="); + Serial.print(pixco[d][0]); + Serial.print("particle x="); + Serial.print(particles[i].x); + Serial.print(" y="); + Serial.println(particles[i].y); + useLocalBuffer = false; + free(colorbuffer); // free buffer memory + } + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) + { + pxlbrightness[d] = -1; // do not render + Serial.print("uncought out of bounds: y="); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[i].x); + Serial.print(" y="); + Serial.println(particles[i].y); + useLocalBuffer = false; + free(colorbuffer); // free buffer memory + } + }*/ + if (useLocalBuffer) { - pixelheat = (dx * precal2) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + if (pxlbrightness[0] > 0) + colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left + if (pxlbrightness[1] > 0) + colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right + if (pxlbrightness[2] > 0) + colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right + if (pxlbrightness[3] > 0) + colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left } - // top right - y++; - if (x < cols && y < rows) + else { - pixelheat = (dx * precal3) >> PS_P_SURFACE; // - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) - } - // top left - x--; - if (wrapX) - { // wrap it to the other side if required - if (x < 0) // left half of particle render is out of frame, wrap it - x = cols - 1; + SEGMENT.fill(BLACK); // clear the matrix + if (pxlbrightness[0] > 0) + SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left + if (pxlbrightness[1] > 0) + SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)pxlbrightness[1])); // bottom right + if (pxlbrightness[2] > 0) + SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right + if (pxlbrightness[3] > 0) + SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // top left + // test to render larger pixels with minimal effort (not working yet, need to calculate coordinate from actual dx position but brightness seems right), could probably be extended to 3x3 + // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd); + // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd); } - if (x < cols && x >= 0 && y < rows) + } + if (useLocalBuffer) + { + uint32_t yflipped; + for (int y = 0; y <= maxYpixel; y++) { - pixelheat = (precal1 * precal3) >> PS_P_SURFACE; - PartMatrix_addHeat(x, y, pixelheat, rows); - // PartMatrix_addHeat(x + 1, y, tempVal, rows); // shift particle by 1 pixel to the right and add heat again (makes flame wider without using more particles) + yflipped = maxYpixel - y; + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColorXY(x, yflipped, colorbuffer[x][y]); + } } + free(colorbuffer); // free buffer memory } } -// adds 'heat' to red color channel, if it overflows, add it to next color channel -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows) -{ - - // const uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - - CRGB currentcolor = SEGMENT.getPixelColorXY(col, rows - row - 1); // read current matrix color (flip y axis) - uint32_t newcolorvalue; - uint32_t colormode = map(SEGMENT.custom3, 0, 31, 0, 5); // get color mode from slider (3bit value) - - // define how the particle TTL value (which is the heat given to the function) maps to heat, if lower, fire is more red, if higher, fire is brighter as bright flames travel higher and decay faster - // need to scale ttl value of particle to a good heat value that decays fast enough - #ifdef ESP8266 - heat = heat << 4; //ESP8266 has no hardware multiplication, just use shift (also less particles, need more heat) - #else - heat = heat * 10; - #endif - // i=0 is normal red fire, i=1 is green fire, i=2 is blue fire - uint32_t i = (colormode & 0x07) >> 1; - i = i % 3; - uint32_t increment = (colormode & 0x01) + 1; // 0 (or 3) means only one single color for the flame, 1 is normal, 2 is alternate color modes - if (currentcolor[i] < 255) - { - newcolorvalue = (uint16_t)currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again - // check if there is heat left over - if (newcolorvalue == 255) - { // there cannot be a leftover if it is not full - heat = heat - (255 - currentcolor[i]); // heat added is difference from current value to full value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value +// calculate pixel positions and brightness distribution for rendering function +// pixelpositions are the physical positions in the matrix that the particle renders to (4x2 array for the four positions) +void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]) +{ + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particle->x - PS_P_HALFRADIUS; + int32_t yoffset = particle->y - PS_P_HALFRADIUS; + int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space + int32_t dy = yoffset % PS_P_RADIUS; + int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + int32_t y = yoffset >> PS_P_RADIUS_SHIFT; + + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] + pixelpositions[0][0] = pixelpositions[3][0] = x; // bottom left & top left + pixelpositions[0][1] = pixelpositions[1][1] = y; // bottom left & bottom right + pixelpositions[1][0] = pixelpositions[2][0] = x + 1; // bottom right & top right + pixelpositions[2][1] = pixelpositions[3][1] = y + 1; // top right & top left + + // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) + + if (x < 0) // left pixels out of frame + { + dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) + //note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) + //checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: + if (dx == PS_P_RADIUS) + { + pixelvalues[1] = pixelvalues[2] = -1; // pixel is actually out of matrix boundaries, do not render } + if (particlesettings.wrapX) // wrap x to the other side if required + pixelpositions[0][0] = pixelpositions[3][0] = maxXpixel; + else + pixelvalues[0] = pixelvalues[3] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixelpositions[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow + { + if (particlesettings.wrapX) // wrap y to the other side if required + pixelpositions[1][0] = pixelpositions[2][0] = 0; else + pixelvalues[1] = pixelvalues[2] = -1; + } + + if (y < 0) // bottom pixels out of frame + { + dy = PS_P_RADIUS + dy; //see note above + if (dy == PS_P_RADIUS) { - heat = 0; // no heat left + pixelvalues[2] = pixelvalues[3] = -1; // pixel is actually out of matrix boundaries, do not render } - currentcolor[i] = (uint8_t)newcolorvalue; + if (particlesettings.wrapY) // wrap y to the other side if required + pixelpositions[0][1] = pixelpositions[1][1] = maxYpixel; + else + pixelvalues[0] = pixelvalues[1] = -1; } - - if (heat > 0) // there is still heat left to be added + else if (pixelpositions[2][1] > maxYpixel) // top pixels { - i += increment; - i = i % 3; + if (particlesettings.wrapY) // wrap y to the other side if required + pixelpositions[2][1] = pixelpositions[3][1] = 0; + else + pixelvalues[2] = pixelvalues[3] = -1; + } - if (currentcolor[i] < 255) + // calculate brightness values for all four pixels representing a particle using linear interpolation + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess; + int32_t precal3 = dy * brightess; + + //calculate the values for pixels that are in frame + if (pixelvalues[0] >= 0) + pixelvalues[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pixelvalues[1] >= 0) + pixelvalues[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pixelvalues[2] >= 0) + pixelvalues[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pixelvalues[3] >= 0) + pixelvalues[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE +/* + Serial.print("x:"); + Serial.print(particle->x); + Serial.print(" y:"); + Serial.print(particle->y); + //Serial.print(" xo"); + //Serial.print(xoffset); + //Serial.print(" dx"); + //Serial.print(dx); + //Serial.print(" "); + for(uint8_t t = 0; t<4; t++) + { + Serial.print(" v"); + Serial.print(pixelvalues[t]); + Serial.print(" x"); + Serial.print(pixelpositions[t][0]); + Serial.print(" y"); + Serial.print(pixelpositions[t][1]); + + Serial.print(" "); + } + Serial.println(" "); +*/ +/* + // debug: check coordinates if out of buffer boundaries print out some info + for (uint32_t d = 0; d < 4; d++) + { + if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel) { - newcolorvalue = (uint32_t)currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)255); // limit to 8bit value again - // check if there is heat left over - if (newcolorvalue == 255) // there cannot be a leftover if red is not full - { - heat = heat - (255 - currentcolor[i]); // heat added is difference from current red value to full red value, subtract it from the inital heat value so heat is the remaining heat not added yet - // this cannot produce an underflow since we never add more than the initial heat value + //Serial.print("<"); + if (pixelvalues[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixelpositions[d][0]); + Serial.print(" y:"); + Serial.print(pixelpositions[d][1]); + Serial.print("particle x="); + Serial.print(particle->x); + Serial.print(" y="); + Serial.println(particle->y); + pixelvalues[d] = -1; // do not render } - else - { - heat = 0; // no heat left + } + if (pixelpositions[d][1] < 0 || pixelpositions[d][1] > maxYpixel) + { + //Serial.print("^"); + if (pixelvalues[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixelpositions[d][0]); + Serial.print(" y:"); + Serial.print(pixelpositions[d][1]); + Serial.print("particle x="); + Serial.print(particle->x); + Serial.print(" y="); + Serial.println(particle->y); + pixelvalues[d] = -1; // do not render } - currentcolor[i] = (uint8_t)newcolorvalue; } } - if (heat > 0) // there is still heat left to be added +*/ +} + +// update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true +// particles move upwards faster if ttl is high (i.e. they are hotter) +void ParticleSystem::fireParticleupdate() +{ + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first and check again + //todo: kill out of bounds funktioniert nicht? + uint32_t i = 0; + + for (i = 0; i < usedParticles; i++) { - i += increment; - i = i % 3; - if (currentcolor[i] < 255) + if (particles[i].ttl > 0) { - newcolorvalue = currentcolor[i] + heat; // add heat, check if it overflows - newcolorvalue = min(newcolorvalue, (uint32_t)50); // limit so it does not go full white - currentcolor[i] = (uint8_t)newcolorvalue; + // age + particles[i].ttl--; + // apply velocity + particles[i].x = particles[i].x + (int32_t)particles[i].vx; + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //!! shift ttl by 2 is the original value, this is experimental + particles[i].outofbounds = 0; + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds + // y-direction + if (particles[i].y < -PS_P_HALFRADIUS) + particles[i].outofbounds = 1; + else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now + { + if ((particles[i].x < 0) || (particles[i].x > maxX)) + { + if (particlesettings.wrapX) + { + particles[i].x = wraparound(particles[i].x, maxX); + } + else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view + { + particles[i].ttl = 0; + } + } + } } } - - SEGMENT.setPixelColorXY(col, rows - row - 1, currentcolor); } + // detect collisions in an array of particles and handle them -void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness) +void ParticleSystem::handleCollisions() { // detect and handle collisions uint32_t i, j; uint32_t startparticle = 0; - uint32_t endparticle = numparticles >> 1; // do half the particles, significantly speeds things up + uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - if (SEGMENT.call % 2 == 0) - { // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // if m ore accurate collisions are needed, just call it twice in a row + if (collisioncounter & 0x01) + { startparticle = endparticle; - endparticle = numparticles; - } + endparticle = usedParticles; + } + collisioncounter++; + + //startparticle = 0;//!!! test: do all collisions every frame, FPS goes from about 52 to + //endparticle = usedParticles; for (i = startparticle; i < endparticle; i++) { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].collide && particles[i].outofbounds == 0) // if particle is alive and does collide and is not out of view + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles - for (j = i + 1; j < numparticles; j++) + for (j = i + 1; j < usedParticles; j++) { // check against higher number particles if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; - if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS))) // check x direction, if close, check y direction + if (dx < PS_P_HARDRADIUS && dx > -PS_P_HARDRADIUS) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; - if ((dx < (PS_P_HARDRADIUS)) && (dx > (-PS_P_HARDRADIUS)) && (dy < (PS_P_HARDRADIUS)) && (dy > (-PS_P_HARDRADIUS))) - { // particles are close - handleCollision(&particles[i], &particles[j], hardness); - } + if (dy < PS_P_HARDRADIUS && dy > -PS_P_HARDRADIUS) // particles are close + collideParticles(&particles[i], &particles[j]); } } } @@ -735,129 +865,404 @@ void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hard } } + + // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness) +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles { - int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; + int32_t dy = particle2->y - particle1->y; int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - - if (distanceSquared == 0) // add distance in case particles exactly meet at center, prevents dotProduct=0 (this can only happen if they move towards each other) + // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { - // Adjust positions based on relative velocity direction TODO: is this really needed? only happens on fast particles, would save some code (but make it a tiny bit less accurate on fast particles but probably not an issue) - - if (relativeVx < 0) - { // if true, particle2 is on the right side - particle1->x--; - particle2->x++; - } - else + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if(relativeVx == 0) //if true { - particle1->x++; - particle2->x--; - } - + relativeVx = 1; + } + + dy = -1; if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) { - particle1->y--; - particle2->y++; + relativeVy = 1; } - else - { - particle1->y++; - particle2->y--; - } - distanceSquared++; + + distanceSquared = 2; //1 + 1 } + // Calculate dot product of relative velocity and relative distance - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); + + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); //is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number - // If particles are moving towards each other - if (dotProduct < 0) + if (dotProduct < 0) // particles are moving towards each other { - const uint32_t bitshift = 14; // bitshift used to avoid floats - + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - int32_t impulse = (((dotProduct << (bitshift)) / (distanceSquared)) * (hardness+1)) >> 8; - int32_t ximpulse = (impulse * dx) >> bitshift; - int32_t yimpulse = (impulse * dy) >> bitshift; + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; //cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - /* - //TODO: this is removed for now as it does not seem to do much and does not help with piling. if soft, much energy is lost anyway at a collision, so they are automatically sticky - //also second version using multiplication is slower on ESP8266 than the if's - if (hardness < 50) // if particles are soft, they become 'sticky' i.e. they are slowed down at collisions + + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction { - - //particle1->vx = (particle1->vx < 2 && particle1->vx > -2) ? 0 : particle1->vx; - //particle1->vy = (particle1->vy < 2 && particle1->vy > -2) ? 0 : particle1->vy; + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vy = ((int32_t)particle1->vy * coeff) / 255; - //particle2->vx = (particle2->vx < 2 && particle2->vx > -2) ? 0 : particle2->vx; - //particle2->vy = (particle2->vy < 2 && particle2->vy > -2) ? 0 : particle2->vy; + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - const uint32_t coeff = 100; - particle1->vx = ((int32_t)particle1->vx * coeff) >> 8; - particle1->vy = ((int32_t)particle1->vy * coeff) >> 8; + if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; - particle2->vx = ((int32_t)particle2->vx * coeff) >> 8; - particle2->vy = ((int32_t)particle2->vy * coeff) >> 8; - }*/ - } + particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; + } + } - // particles have volume, push particles apart if they are too close by moving each particle by a fixed amount away from the other particle - // if pushing is made dependent on hardness, things start to oscillate much more, better to just add a fixed, small increment (tried lots of configurations, this one works best) - // one problem remaining is, particles get squished if (external) force applied is higher than the pushback but this may also be desirable if particles are soft. also some oscillations cannot be avoided without addigng a counter - if (distanceSquared < 2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS) - { - uint8_t choice = dotProduct & 0x01; // random16(2); // randomly choose one particle to push, avoids oscillations note: dotprouct LSB should be somewhat random, so no need to calculate a random number - const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; - const int32_t pushamount = 2; //push a small amount - int32_t push = pushamount; + // this part is for particle piling: slow them down if they are close (they become sticky) and push them so they counteract gravity + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + { + /** + //only apply friction if particles are slow or else fast moving particles (as in explosions) get slowed a lot + relativeVy *= relativeVy; //square the speed, apply friction if speed is below 10 + if (relativeVy < 100) //particles are slow in y direction -> this works but most animations look much nicer without this friction. add friction in FX if required. + { + //now check x as well (no need to check if y speed is high, this saves some computation time) + relativeVx *= relativeVx; // square the speed, apply friction if speed is below 10 + if (relativeVx < 100) // particles are slow in x direction + { + particle1->vx = ((int32_t)particle1->vx * 254) / 256; + particle2->vx = ((int32_t)particle2->vx * 254) / 256; + + particle1->vy = ((int32_t)particle1->vy * 254) / 256; + particle2->vy = ((int32_t)particle2->vy * 254) / 256; - if (dx < HARDDIAMETER && dx > -HARDDIAMETER) - { // distance is too small, push them apart + } + }*/ + + + + // const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really + // int32_t push = (2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push; + + // if (dx < HARDDIAMETER && dx > -HARDDIAMETER) //this is always true as it is checked before ntering this function! + { // distance is too small, push them apart + push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; //(HARDDIAMETER + dx) / 4; + else if (dx > 0) + push = -pushamount; //-(HARDDIAMETER - dx) / 4; + else // on the same x coordinate, shift it a little so they do not stack + { - if (dx <= 0) - push = -pushamount; //-(PS_P_HARDRADIUS + dx); // inverted push direction + if (notsorandom) + particle1->x++; // move it so pile collapses + else + particle1->x--; + } - if (choice) // chose one of the particles to push, avoids oscillations - particle1->x -= push; - else - particle2->x += push; + particle1->vx += push; + } + + // if (dy < HARDDIAMETER && dy > -HARDDIAMETER) //dito + { + push = 0; + if (dy < 0) + push = pushamount; //(HARDDIAMETER + dy) / 4; + else if (dy > 0) + push = -pushamount; //-(HARDDIAMETER - dy) / 4; + else // dy==0 + { + if (notsorandom) + particle1->y++; // move it so pile collapses + else + particle1->y--; + } + + particle1->vy += push; + } + /* + if (dx < HARDDIAMETER && dx > -HARDDIAMETER) + { // distance is too small, push them apart + push = 0; + if (dx < 0) // particle 1 is on the right + push = 2; //(HARDDIAMETER + dx) / 4; + else if (dx > 0) + push = -2; //-(HARDDIAMETER - dx) / 4; + else //on the same x coordinate, shift it a little so they do not stack + particle1->x += 2; + if (notsorandom) // chose one of the particles to push, avoids oscillations + { + if (!particle1->flag3) + { + particle1->vx += push; + particle1->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle1->flag3 = 0; //reset + } + else + { + if (!particle2->flag3) + { + particle2->vx -= push; + particle2->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle2->flag3 = 0; // reset + } + } + + if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + { + push = 0; + if (dy < 0) + push = 2; //(HARDDIAMETER + dy) / 4; + else if (dy > 0) + push = -2; //-(HARDDIAMETER - dy) / 4; + + if (!notsorandom) // chose one of the particles to push, avoids oscillations + { + if (!particle1->flag3) + { + particle1->vy += push; + particle1->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle1->flag3 = 0; // reset + } + else + { + if (!particle2->flag3) + { + particle2->vy -= push; + particle2->flag3 = 1; // particle was pushed, is reset on next push request + } + else + particle2->flag3 = 0; // reset + } + }*/ + + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } + } - push = pushamount; // reset push variable to 1 - if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + +} + +//fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266) +//function assumes that out of bounds is checked before calling it +int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) +{ + if (p < 0) + { + p += maxvalue + 1; + } + else //if (p > maxvalue) + { + p -= maxvalue + 1; + } + return p; +} + +//calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +//force is in 3.4 fixedpoint notation, +/-127 +int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) +{ + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) + { + *counter += force_abs; + if (*counter > 15) { - if (dy <= 0) - push = -pushamount; //-(PS_P_HARDRADIUS + dy); // inverted push direction + *counter -= 16; + dv = (force < 0) ? -1 : ((force > 0) ? 1 : 0); // force is either, 1, 0 or -1 if it is small + } + } + else + { + dv = force >> 4; // MSBs + } + return dv; +} - if (choice) // chose one of the particles to push, avoids oscillations - particle1->y -= push; - else - particle2->y += push; +// allocate memory for the 2D array in one contiguous block and set values to zero +CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) +{ + cli();//!!! test to see if anything messes with the allocation (flicker issues) + CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + sei(); + if (array2D == NULL) + DEBUG_PRINT(F("PS buffer alloc failed")); + else + { + // assign pointers of 2D array + CRGB *start = (CRGB *)(array2D + cols); + for (uint i = 0; i < cols; i++) + { + array2D[i] = start + i * rows; } - // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero + } + return array2D; +} + +//update size and pointers (memory location and size can change dynamically) +//note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +void ParticleSystem::updateSystem(void) +{ + // update matrix size + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + setMatrixSize(cols, rows); + updatePSpointers(); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem::updatePSpointers() +{ + //DEBUG_PRINT(F("*** PS pointers ***")); + //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS + + //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); + //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); + //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); +} + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles() +{ + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + uint numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint numberofParticles = (cols * rows); // 1 particle per pixe + uint particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint numberofParticles = (cols * rows); // 1 particle per pixel (for example 768 particles on 32x16) + uint particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + numberofParticles = max((uint)1, min(numberofParticles, particlelimit)); + return numberofParticles; +} + +uint32_t calculateNumberOfSources() +{ + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); +#ifdef ESP8266 + int numberofSources = (cols * rows) / 8; + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = (cols * rows) / 6; + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 +#else + int numberofSources = (cols * rows) / 4; + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 72 +#endif + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) +{ + uint32_t requiredmemory = sizeof(ParticleSystem); + requiredmemory += sizeof(PSparticle) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + Serial.print("allocating: "); + Serial.print(requiredmemory); + Serial.println("Bytes"); + Serial.print("allocating for segment at"); + Serial.println((uintptr_t)SEGMENT.data); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) +{ + Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles(); + uint32_t numsources = calculateNumberOfSources(); + if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; } + Serial.print("segment.data ptr"); + Serial.println((uintptr_t)(SEGMENT.data)); + uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + Serial.println("calling constructor"); + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources); // particle system constructor TODO: why does VS studio thinkt this is bad? + Serial.print("PS pointer at "); + Serial.println((uintptr_t)PartSys); + return true; } -// slow down particle by friction, the higher the speed, the higher the friction coefficient must be <255 or friction is flipped -void applyFriction(PSparticle *particle, int32_t coefficient) +// fastled color adding is very inaccurate in color preservation +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) +CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) { - //note: to increase calculation efficiency, coefficient is not checked if it is within necessary limits of 0-255! if coefficient is made < 1 particles speed up! - coefficient = (int32_t)255 - coefficient; - if (particle->ttl) + CRGB result; + scale++; //add one to scale so 255 will not scale when shifting + uint32_t r = c1.r + ((c2.r * (scale)) >> 8); + uint32_t g = c1.g + ((c2.g * (scale)) >> 8); + uint32_t b = c1.b + ((c2.b * (scale)) >> 8); + uint32_t max = r; + if (g > max) //note: using ? operator would be slower by 2 cpu cycles + max = g; + if (b > max) + max = b; + if (max < 256) + { + result.r = r; + result.g = g; + result.b = b; + } + else { - particle->vx = ((int16_t)particle->vx * coefficient) >> 8; - particle->vy = ((int16_t)particle->vy * coefficient) >> 8; + result.r = (r * 255) / max; + result.g = (g * 255) / max; + result.b = (b * 255) / max; } + return result; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index f4c2163757..af8c37d1bc 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -3,7 +3,6 @@ Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. by DedeHai (Damian Schneider) 2013-2024 - Rendering is based on algorithm by giladaya, https://github.com/giladaya/arduino-particle-sys LICENSE The MIT License (MIT) @@ -28,6 +27,15 @@ #include +#include "FastLED.h" + +//memory allocation +#define ESP8266_MAXPARTICLES 148 // enough for one 16x16 segment with transitions +#define ESP8266_MAXSOURCES 16 +#define ESP32S2_MAXPARTICLES 768 // enough for four 16x16 segments +#define ESP32S2_MAXSOURCES 48 +#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... +#define ESP32_MAXSOURCES 64 //particle dimensions (subpixel division) #define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) @@ -35,6 +43,8 @@ #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity +#define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky +#define PS_P_MAXSPEED 200 //maximum speed a particle can have //struct for a single particle typedef struct { @@ -42,41 +52,123 @@ typedef struct { int16_t y; //y position in particle system int8_t vx; //horizontal velocity int8_t vy; //vertical velocity - uint16_t ttl; // time to live uint8_t hue; // color hue uint8_t sat; // color saturation - //add a one byte bit field: + //two byte bit field: + uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area - bool collide : 1; //if flag is set, particle will take part in collisions - bool flag2 : 1; // unused flags... could use one for collisions to make those selective. - bool flag3 : 1; - uint8_t counter : 4; //a 4 bit counter for particle control + bool collide : 1; //if set, particle takes part in collisions + bool flag3 : 1; // unused flags... + bool flag4 : 1; } PSparticle; //struct for a particle source typedef struct { uint16_t minLife; //minimum ttl of emittet particles uint16_t maxLife; //maximum ttl of emitted particles - PSparticle source; //use a particle as the emitter source (speed, position, color) - uint8_t var; //variation of emitted speed - int8_t vx; //emitting speed - int8_t vy; //emitting speed -} PSpointsource; - -#define GRAVITYCOUNTER 2 //the higher the value the lower the gravity (speed is increased every n'th particle update call), values of 1 to 4 give good results -#define MAXGRAVITYSPEED 40 //particle terminal velocity - -void Emitter_Flame_emit(PSpointsource *emitter, PSparticle *part); -void Emitter_Fountain_emit(PSpointsource *emitter, PSparticle *part); -void Emitter_Angle_emit(PSpointsource *emitter, PSparticle *part, uint8_t angle, uint8_t speed); -void Particle_attractor(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); -void Particle_Move_update(PSparticle *part, bool killoutofbounds = false, bool wrapX = false, bool wrapY = false); -void Particle_Bounce_update(PSparticle *part, const uint8_t hardness); -void Particle_Gravity_update(PSparticle *part, bool wrapX, bool bounceX, bool bounceY, const uint8_t hardness); -void ParticleSys_render(PSparticle *particles, uint32_t numParticles, bool wrapX, bool wrapY); -void FireParticle_update(PSparticle *part, uint32_t numparticles, bool wrapX = false); -void ParticleSys_renderParticleFire(PSparticle *particles, uint32_t numParticles, bool wrapX); -void PartMatrix_addHeat(uint8_t col, uint8_t row, uint32_t heat, uint32_t rows); -void detectCollisions(PSparticle *particles, uint32_t numparticles, uint8_t hardness); -void handleCollision(PSparticle *particle1, PSparticle *particle2, const uint32_t hardness); -void applyFriction(PSparticle *particle, int32_t coefficient); + PSparticle source; //use a particle as the emitter source (speed, position, color) + uint8_t var; //variation of emitted speed + int8_t vx; //emitting speed + int8_t vy; //emitting speed +} PSsource; + +// struct for PS settings +typedef struct +{ + // add a one byte bit field: + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function +} PSsettings; + +class ParticleSystem +{ +public: + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateFire(uint32_t intensity); // update function for fire + + + // particle emitters + void flameEmit(PSsource &emitter); + void sprayEmit(PSsource &emitter); + void angleEmit(PSsource& emitter, uint16_t angle, uint32_t speed); + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + + // move functions + void particleMoveUpdate(PSparticle &part, PSsettings &options); + + //particle physics + void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter); + void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce + void applyGravity(PSparticle *part); //use global system settings + void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); + void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce); + void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); + void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle); + void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle + void applyFriction(uint8_t coefficient); // apply friction to all used particles + void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); + + //set options + void setUsedParticles(uint16_t num); + void setCollisionHardness(uint8_t hardness); //hardness for particle collisions (255 means full hard) + void setWallHardness(uint8_t hardness); //hardness for bouncing on the wall if bounceXY is set + void setMatrixSize(uint16_t x, uint16_t y); + void setWrapX(bool enable); + void setWrapY(bool enable); + void setBounceX(bool enable); + void setBounceY(bool enable); + void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die + void setColorByAge(bool enable); + void enableGravity(bool enable, uint8_t force = 8); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); + + PSparticle *particles; // pointer to particle array + PSsource *sources; // pointer to sources + uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data + uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required) + uint8_t numSources; //number of sources + uint16_t numParticles; // number of particles available in this system + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + +private: + //rendering functions + void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); + void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); + + //paricle physics applied by system if flags are set + void handleCollisions(); + void collideParticles(PSparticle *particle1, PSparticle *particle2); + void fireParticleupdate(); + + //utility functions + void updatePSpointers(); // update the data pointers to current segment data space + int32_t wraparound(int32_t w, int32_t maxvalue); + int32_t calcForce_dV(int8_t force, uint8_t *counter); + CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); + + // note: variables that are accessed often are 32bit for speed + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + int32_t wallHardness; + uint8_t gforcecounter; //counter for global gravity + uint8_t gforce; //gravity strength, default is 8 + uint8_t collisioncounter; //counter to handle collisions +}; + +//initialization functions (not part of class) +bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes = 0); +uint32_t calculateNumberOfParticles(); +uint32_t calculateNumberOfSources(); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +//color add function +CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file From a0452bb3c6128fb7cc0127b22fd0d6fd82fb2e7c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 30 Apr 2024 19:50:12 +0200 Subject: [PATCH 11/21] Added more rendering options and fixed countless bugs Additions: - Motion blurring on local buffer - Blurring / smearing on local buffer - Fast color scaling function - Blurring of individual particles - Advanced settings for particles (forces, render size) - Wall roughness setting for randomized wall bounce - Ghost Rider FX implementation for PS Changes: - Transferred all existing FX to PS class - Improvements on most FX, also nicer rendering with blurring - Improvements in PS Fire FX - Added larger size particle rendering to Ballpit FX - Added random size Particle rendering to Ballpit FX (set to size 255) --- .github/workflows/wled-ci.yml | 4 +- CHANGELOG.md | 82 +- CONTRIBUTING.md | 10 + package-lock.json | 102 +- package.json | 5 +- pio-scripts/output_bins.py | 60 +- platformio.ini | 186 +-- platformio_override.sample.ini | 73 +- requirements.txt | 2 +- tools/WLED_ESP32-wrover_4MB.csv | 6 +- tools/WLED_ESP32_4MB_256KB_FS.csv | 7 + tools/WLED_ESP32_4MB_700k_FS.csv | 6 + tools/cdata-test.js | 9 +- tools/cdata.js | 10 +- usermods/BH1750_v2/usermod_bh1750.h | 2 +- usermods/MAX17048_v2/readme.md | 64 + usermods/MAX17048_v2/usermod_max17048.h | 281 +++++ usermods/TetrisAI_v2/gridbw.h | 117 ++ usermods/TetrisAI_v2/gridcolor.h | 132 ++ usermods/TetrisAI_v2/pieces.h | 184 +++ usermods/TetrisAI_v2/rating.h | 64 + usermods/TetrisAI_v2/readme.md | 33 + usermods/TetrisAI_v2/tetrisai.h | 302 +++++ usermods/TetrisAI_v2/tetrisaigame.h | 150 +++ usermods/TetrisAI_v2/tetrisbag.h | 100 ++ usermods/TetrisAI_v2/usermod_v2_tetrisai.h | 222 ++++ usermods/audioreactive/audio_source.h | 4 +- usermods/audioreactive/readme.md | 15 +- usermods/multi_relay/usermod_multi_relay.h | 2 +- .../readme.md | 63 +- .../usermod_v2_four_line_display_ALT.h | 61 +- ...platformio\342\200\223override.sample.ini" | 17 + .../readme.md | 35 +- .../usermod_v2_rotary_encoder_ui_ALT.h | 16 +- wled00/FX.cpp | 1103 +++++++++++------ wled00/FX.h | 33 +- wled00/FX_2Dfcn.cpp | 199 +-- wled00/FX_fcn.cpp | 234 ++-- wled00/FXparticleSystem.cpp | 1082 ++++++++++------ wled00/FXparticleSystem.h | 146 ++- wled00/bus_manager.cpp | 133 +- wled00/bus_manager.h | 57 +- wled00/bus_wrapper.h | 345 ++++-- wled00/button.cpp | 46 +- wled00/cfg.cpp | 23 +- wled00/colors.cpp | 36 +- wled00/const.h | 32 +- wled00/data/index.css | 2 +- wled00/data/index.js | 29 +- wled00/data/settings_leds.htm | 25 +- wled00/data/settings_wifi.htm | 54 +- wled00/e131.cpp | 19 +- wled00/fcn_declare.h | 3 +- wled00/file.cpp | 38 +- wled00/improv.cpp | 2 +- wled00/json.cpp | 102 +- wled00/led.cpp | 7 +- wled00/mqtt.cpp | 7 +- wled00/ntp.cpp | 3 - wled00/pin_manager.cpp | 17 +- wled00/pin_manager.h | 9 +- wled00/playlist.cpp | 14 +- wled00/presets.cpp | 43 +- wled00/remote.cpp | 1 - wled00/set.cpp | 28 +- wled00/src/dependencies/json/AsyncJson-v6.h | 6 +- wled00/usermods_list.cpp | 16 + wled00/util.cpp | 6 +- wled00/wled.cpp | 57 +- wled00/wled.h | 57 +- wled00/wled_server.cpp | 107 +- wled00/ws.cpp | 25 +- wled00/xml.cpp | 17 +- 73 files changed, 4719 insertions(+), 1870 deletions(-) create mode 100644 tools/WLED_ESP32_4MB_256KB_FS.csv create mode 100644 tools/WLED_ESP32_4MB_700k_FS.csv create mode 100644 usermods/MAX17048_v2/readme.md create mode 100644 usermods/MAX17048_v2/usermod_max17048.h create mode 100644 usermods/TetrisAI_v2/gridbw.h create mode 100644 usermods/TetrisAI_v2/gridcolor.h create mode 100644 usermods/TetrisAI_v2/pieces.h create mode 100644 usermods/TetrisAI_v2/rating.h create mode 100644 usermods/TetrisAI_v2/readme.md create mode 100644 usermods/TetrisAI_v2/tetrisai.h create mode 100644 usermods/TetrisAI_v2/tetrisaigame.h create mode 100644 usermods/TetrisAI_v2/tetrisbag.h create mode 100644 usermods/TetrisAI_v2/usermod_v2_tetrisai.h create mode 100644 "usermods/usermod_v2_rotary_encoder_ui_ALT/platformio\342\200\223override.sample.ini" diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml index f9ffb64816..1dcab26ab4 100644 --- a/.github/workflows/wled-ci.yml +++ b/.github/workflows/wled-ci.yml @@ -37,7 +37,7 @@ jobs: uses: actions/setup-node@v4 with: cache: 'npm' - - run: npm install + - run: npm ci - name: Cache PlatformIO uses: actions/cache@v4 with: @@ -61,7 +61,7 @@ jobs: name: firmware-${{ matrix.environment }} path: | build_output/release/*.bin - build_output/release/*_ESP02.bin.gz + build_output/release/*_ESP02*.bin.gz release: name: Create Release runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d432e357d..59c58dfa33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,65 @@ ## WLED changelog +#### Build 2404120 +- v0.15.0-b3 +- fix for #3896 & WS2815 current saving +- conditional compile for AA setPixelColor() + +#### Build 2404100 +- Internals: #3859, #3862, #3873, #3875 +- Prefer I2S1 over RMT on ESP32 +- usermod for Adafruit MAX17048 (#3667 by @ccruz09) +- Runtime detection of ESP32 PICO, general PSRAM support +- Extend JSON API "info" object + - add "clock" - CPU clock in MHz + - add "flash" - flash size in MB +- Fix for #3879 +- Analog PWM fix for ESP8266 (#3887 by @gaaat98) +- Fix for #3870 (#3880 by @DedeHai) +- ESP32 S3/S2 touch fix (#3798 by @DedeHai) +- PIO env. PSRAM fix for S3 & S3 with 4M flash + - audioreactive always included for S3 & S2 +- Fix for #3889 +- BREAKING: Effect: modified KITT (Scanner) (#3763) + +#### Build 2403280 +- Individual color channel control for JSON API (fixes #3860) + - "col":[int|string|object|array, int|string|object|array, int|string|object|array] + int = Kelvin temperature or 0 for black + string = hex representation of [WW]RRGGBB + object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) + array = direct channel values [r,g,b,w] (w element being optional) +- runtime selection for CCT IC (Athom 15W bulb) +- #3850 (by @w00000dy) +- Rotary encoder palette count bugfix +- bugfixes and optimisations + +#### Build 2403240 +- v0.15.0-b2 +- WS2805 support (RGB + WW + CW, 600kbps) +- Unified PSRAM use +- NeoPixelBus v2.7.9 +- Ubiquitous PSRAM mode for all variants of ESP32 +- SSD1309_64 I2C Support for FLD Usermod (#3836 by @THATDONFC) +- Palette cycling fix (add support for `{"seg":[{"pal":"X~Y~"}]}` or `{"seg":[{"pal":"X~Yr"}]}`) +- FW1906 Support (#3810 by @deece and @Robert-github-com) +- ESPAsyncWebServer 2.2.0 (#3828 by @willmmiles) +- Bugfixes: #3843, #3844 + +#### Build 2403190 +- limit max PWM frequency (fix incorrect PWM resolution) +- Segment UI bugfix +- Updated AsyncWebServer (by @wlillmmiles) +- Simpler boot preset (fix for #3806) +- Effect: Fix for 2D Drift animation (#3816 by @BaptisteHudyma) +- Effect: Add twin option to 2D Drift +- MQTT cleanup +- DDP: Support sources that don't push (#3833 by @willmmiles) +- Usermod: Tetris AI usermod (#3711 by @muebau) + +#### Build 2403171 +- merge 0.14.2 changes into 0.15 + #### Build 2403070 - Add additional segment options when controlling over e1.31 (#3616 by @demophoon) - LockedJsonResponse: Release early if possible (#3760 by @willmmiles) @@ -38,7 +98,7 @@ #### Build 2309120 till build 2402010 - WLED version 0.15.0-a0 -- Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to +- Multi-WiFi support. Add up to 3 (or more via cusom compile) WiFis to connect to (with help from @JPZV) - Temporary AP. Use your WLED in public with temporary AP. - Github CI build system enhancements (#3718 by @WoodyLetsCode) - Accessibility: Node list ( #3715 by @WoodyLetsCode) @@ -120,6 +180,26 @@ - send UDP/WS on segment change - pop_back() when removing last segment +#### Build 2403170 +- WLED 0.14.2 release + +#### Build 2403110 +- Beta WLED 0.14.2-b2 +- New AsyncWebServer (improved performance and reduced memory use) +- New builds for ESP8266 with 160MHz CPU clock +- Fixing stairway usermod and adding buildflags (#3758 by @lost-hope) +- Fixing a potential array bounds violation in ESPDMX +- Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM) +- LockedJsonResponse: Release early if possible (by @willmmiles) + +#### Build 2402120 +- Beta WLED 0.14.2-b1 +- Possible fix for #3589 & partial fix for #3605 +- Prevent JSON buffer clear after failed lock attempt +- Multiple analog button fix for #3549 +- UM Audioreactive: add two compiler options (#3732 by @wled-install) +- Fix for #3693 + #### Build 2401141 - Official release of WLED 0.14.1 - Fix for #3566, #3665, #3672 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddf61ec809..f813999bb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,16 @@ Here are a few suggestions to make it easier for you to contribute! +### Describe your PR + +Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* testing you performed, known limitations, open ends you possibly could not solve. +* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉) + ### Target branch for pull requests Please make all PRs against the `0_15` branch. diff --git a/package-lock.json b/package-lock.json index 99e3efc3d5..b9dc5e0e3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,57 +1,56 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b3", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "inliner": "^1.13.1", - "nodemon": "^3.0.2", - "zlib": "^1.0.5" + "nodemon": "^3.0.2" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -60,9 +59,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.21.tgz", - "integrity": "sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -209,11 +208,14 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/boolbase": { @@ -324,15 +326,9 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -345,6 +341,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1376,9 +1375,9 @@ } }, "node_modules/nodemon": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", - "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", + "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", @@ -1827,9 +1826,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1925,9 +1924,9 @@ } }, "node_modules/stream-shift": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.2.tgz", - "integrity": "sha512-rV4Bovi9xx0BFzOb/X0B2GqoIjvqPCttZdu0Wgtx2Dxkj7ETyWl9gmqJ4EutWRLvtZWm8dxE+InQZX1IryZn/w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, "node_modules/string_decoder": { "version": "0.10.31", @@ -1994,9 +1993,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", + "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -2245,15 +2244,6 @@ "decamelize": "^1.0.0", "window-size": "0.1.0" } - }, - "node_modules/zlib": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zlib/-/zlib-1.0.5.tgz", - "integrity": "sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==", - "hasInstallScript": true, - "engines": { - "node": ">=0.2.0" - } } } } diff --git a/package.json b/package.json index f2c0e3d652..b19ecc48a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b1", + "version": "0.15.0-b3", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { @@ -26,7 +26,6 @@ "clean-css": "^5.3.3", "html-minifier-terser": "^7.2.0", "inliner": "^1.13.1", - "nodemon": "^3.0.2", - "zlib": "^1.0.5" + "nodemon": "^3.0.2" } } diff --git a/pio-scripts/output_bins.py b/pio-scripts/output_bins.py index e12b11c2ca..c0e85dcbb2 100644 --- a/pio-scripts/output_bins.py +++ b/pio-scripts/output_bins.py @@ -4,6 +4,7 @@ import gzip OUTPUT_DIR = "build_output{}".format(os.path.sep) +#OUTPUT_DIR = os.path.join("build_output") def _get_cpp_define_value(env, define): define_list = [item[-1] for item in env["CPPDEFINES"] if item[0] == define] @@ -13,24 +14,24 @@ def _get_cpp_define_value(env, define): return None -def _create_dirs(dirs=["firmware", "map"]): - # check if output directories exist and create if necessary - if not os.path.isdir(OUTPUT_DIR): - os.mkdir(OUTPUT_DIR) - +def _create_dirs(dirs=["map", "release", "firmware"]): for d in dirs: - if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)): - os.mkdir("{}{}".format(OUTPUT_DIR, d)) + os.makedirs(os.path.join(OUTPUT_DIR, d), exist_ok=True) def create_release(source): release_name = _get_cpp_define_value(env, "WLED_RELEASE_NAME") if release_name: - _create_dirs(["release"]) version = _get_cpp_define_value(env, "WLED_VERSION") - # get file extension of source file (.bin or .bin.gz) - ext = source.split(".", 1)[1] - release_file = "{}release{}WLED_{}_{}.{}".format(OUTPUT_DIR, os.path.sep, version, release_name, ext) + release_file = os.path.join(OUTPUT_DIR, "release", f"WLED_{version}_{release_name}.bin") + release_gz_file = release_file + ".gz" + print(f"Copying {source} to {release_file}") shutil.copy(source, release_file) + bin_gzip(release_file, release_gz_file) + else: + variant = env["PIOENV"] + bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + print(f"Copying {source} to {bin_file}") + shutil.copy(source, bin_file) def bin_rename_copy(source, target, env): _create_dirs() @@ -38,38 +39,21 @@ def bin_rename_copy(source, target, env): # create string with location and file names based on variant map_file = "{}map{}{}.map".format(OUTPUT_DIR, os.path.sep, variant) - bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) - - # check if new target files exist and remove if necessary - for f in [map_file, bin_file]: - if os.path.isfile(f): - os.remove(f) - - # copy firmware.bin to firmware/.bin - shutil.copy(str(target[0]), bin_file) - create_release(bin_file) + create_release(str(target[0])) # copy firmware.map to map/.map if os.path.isfile("firmware.map"): shutil.move("firmware.map", map_file) -def bin_gzip(source, target, env): - _create_dirs() - variant = env["PIOENV"] - - # create string with location and file names based on variant - bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) - gzip_file = "{}firmware{}{}.bin.gz".format(OUTPUT_DIR, os.path.sep, variant) - - # check if new target files exist and remove if necessary - if os.path.isfile(gzip_file): os.remove(gzip_file) - - # write gzip firmware file - with open(bin_file,"rb") as fp: - with gzip.open(gzip_file, "wb", compresslevel = 9) as f: +def bin_gzip(source, target): + # only create gzip for esp8266 + if not env["PIOPLATFORM"] == "espressif8266": + return + + print(f"Creating gzip file {target} from {source}") + with open(source,"rb") as fp: + with gzip.open(target, "wb", compresslevel = 9) as f: shutil.copyfileobj(fp, f) - create_release(gzip_file) - -env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_rename_copy, bin_gzip]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", bin_rename_copy) diff --git a/platformio.ini b/platformio.ini index 6306595a29..01615785df 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,13 +10,12 @@ # ------------------------------------------------------------------------------ # CI/release binaries -default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_8MB, esp32s3dev_8MB_PSRAM_opi, esp32_wrover +default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_160, esp01_1m_full_160, esp32dev, esp32_eth, esp32dev_audioreactive, lolin_s2_mini, esp32c3dev, esp32s3dev_16MB_opi, esp32s3dev_8MB_opi, esp32s3_4M_qspi, esp32_wrover src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = - platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ @@ -41,14 +40,13 @@ arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/ platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -platform_packages = platformio/framework-arduinoespressif8266 - platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 ## previous platform for 8266, in case of problems with the new one -## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_2_0, which does not support Ucs890x -;; platform_wled_default = ${common.arduino_core_3_2_0} +## you'll need makuna/NeoPixelBus@ 2.6.9 for arduino_core_3_0_2, which does not support Ucs890x +;; platform_wled_default = ${common.arduino_core_3_0_2} ;; platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 ;; platformio/toolchain-xtensa @ ~2.40802.200502 ;; platformio/tool-esptool @ ~1.413.0 @@ -87,7 +85,6 @@ debug_flags = -D DEBUG=1 -D WLED_DEBUG # This reduces the OTA size with ~45KB, so it's especially useful on low memory boards (512k/1m). # ------------------------------------------------------------------------------ build_flags = - -Wno-attributes -DMQTT_MAX_PACKET_SIZE=1024 -DSECURE_CLIENT=SECURE_CLIENT_BEARSSL -DBEARSSL_SSL_BASIC @@ -105,10 +102,6 @@ build_flags = build_unflags = -build_flags_esp8266 = ${common.build_flags} ${esp8266.build_flags} -build_flags_esp32 = ${common.build_flags} ${esp32.build_flags} -build_flags_esp32_V4= ${common.build_flags} ${esp32_idf_V4.build_flags} - ldscript_1m128k = eagle.flash.1m128.ld ldscript_2m512k = eagle.flash.2m512.ld ldscript_2m1m = eagle.flash.2m1m.ld @@ -143,8 +136,8 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.5 - https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.1.0 + makuna/NeoPixelBus @ 2.7.9 + https://github.com/Aircoookie/ESPAsyncWebServer.git @ 2.2.1 # for I2C interface ;Wire # ESP-NOW library @@ -164,6 +157,9 @@ lib_deps = #For ADS1115 sensor uncomment following ;adafruit/Adafruit BusIO @ 1.13.2 ;adafruit/Adafruit ADS1X15 @ 2.4.0 + #For MAX1704x Lipo Monitor / Fuel Gauge uncomment following + ; https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 + ; https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 #For MPU6050 IMU uncomment follwoing ;electroniccats/MPU6050 @1.0.1 # For -D USERMOD_ANIMARTRIX @@ -172,7 +168,7 @@ lib_deps = # SHT85 ;robtillaart/SHT85@~0.3.3 # Audioreactive usermod - ;kosme/arduinoFFT @ 2.0.0 + ;kosme/arduinoFFT @ 2.0.1 extra_scripts = ${scripts_defaults.extra_scripts} @@ -216,14 +212,19 @@ build_flags = -g #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x -D LOROL_LITTLEFS ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 +tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv +big_partitions = tools/WLED_ESP32_4MB_256KB_FS.csv ;; 1.8MB firmware, 256KB filesystem, coredump support +large_partitions = tools/WLED_ESP32_8MB.csv +extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} # additional build flags for audioreactive AR_build_flags = -D USERMOD_AUDIOREACTIVE -AR_lib_deps = kosme/arduinoFFT @ 2.0.0 +AR_lib_deps = kosme/arduinoFFT @ 2.0.1 [esp32_idf_V4] ;; experimental build environment for ESP32 using ESP-IDF 4.4.x / arduino-esp32 v2.0.5 @@ -231,24 +232,21 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.0 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - #-DCONFIG_LITTLEFS_FOR_IDF_3_2 -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} [esp32s2] ;; generic definitions for all ESP32-S2 boards -platform = espressif32@5.3.0 -platform_packages = -default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 @@ -265,8 +263,8 @@ lib_deps = [esp32c3] ;; generic definitions for all ESP32-C3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 @@ -282,8 +280,8 @@ lib_deps = [esp32s3] ;; generic definitions for all ESP32-S3 boards -platform = espressif32@5.3.0 -platform_packages = +platform = espressif32@ ~6.3.2 +platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g -DESP32 -DARDUINO_ARCH_ESP32 @@ -294,7 +292,6 @@ build_flags = -g -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT - lib_deps = https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 ${env.lib_deps} @@ -310,20 +307,14 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder [env:nodemcuv2_160] -board = nodemcuv2 -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_4m1m} +extends = env:nodemcuv2 board_build.f_cpu = 160000000L -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D -lib_deps = ${esp8266.lib_deps} -monitor_filters = esp8266_exception_decoder +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D [env:esp8266_2m] board = esp_wroom_02 @@ -331,18 +322,13 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_160] -board = esp_wroom_02 -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_2m512k} +extends = env:esp8266_2m board_build.f_cpu = 160000000L -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 -lib_deps = ${esp8266.lib_deps} +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP02_160 [env:esp01_1m_full] board = esp01_1m @@ -350,37 +336,45 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_160] -board = esp01_1m -platform = ${common.platform_wled_default} -platform_packages = ${common.platform_packages} -board_build.ldscript = ${common.ldscript_1m128k} +extends = env:esp01_1m_full board_build.f_cpu = 160000000L -build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM -lib_deps = ${esp8266.lib_deps} [env:esp32dev] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} +[env:esp32dev_8M] +board = esp32dev +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_8M #-D WLED_DISABLE_BROWNOUT_DET + ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ${esp32.AR_lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.large_partitions} +; board_build.f_flash = 80000000L + [env:esp32dev_audioreactive] board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_audioreactive #-D WLED_DISABLE_BROWNOUT_DET ${esp32.AR_build_flags} lib_deps = ${esp32.lib_deps} ${esp32.AR_lib_deps} @@ -395,23 +389,26 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32_Ethernet -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} [env:esp32_wrover] -platform = ${esp32.platform} +extends = esp32_idf_V4 +platform = ${esp32_idf_V4.platform} +platform_packages = ${esp32_idf_V4.platform_packages} board = ttgo-t7-v14-mini32 board_build.f_flash = 80000000L board_build.flash_mode = qio board_build.partitions = ${esp32.default_partitions} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_WROVER - -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue - -D WLED_USE_PSRAM +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_WROVER + -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ;; Older ESP32 (rev.<3) need a PSRAM fix (increases static RAM used) https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/external-ram.html -D LEDPIN=25 -lib_deps = ${esp32.lib_deps} + ; ${esp32.AR_build_flags} +lib_deps = ${esp32_idf_V4.lib_deps} + ; ${esp32.AR_lib_deps} [env:esp32c3dev] extends = esp32c3 @@ -419,36 +416,38 @@ platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} framework = arduino board = esp32-c3-devkitm-1 -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv +board_build.partitions = ${esp32.default_partitions} build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME=ESP32-C3 -D WLED_WATCHDOG_TIMEOUT=0 -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 +upload_speed = 460800 ; 115200 230400 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} -[env:esp32s3dev_8MB] -;; ESP32-S3-DevKitC-1 development board, with 8MB FLASH, no PSRAM (flash_mode: qio) -board = esp32-s3-devkitc-1 +[env:esp32s3dev_16MB_opi] +;; ESP32-S3 development board, with 16MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) +board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support +board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} -upload_speed = 921600 ; or 460800 +upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_16MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 - -D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip - ;-D ARDUINO_USB_CDC_ON_BOOT=1 ;; -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - ;-D WLED_DEBUG + ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.extreme_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio -; board_build.flash_mode = dio ;; try this if you have problems at startup monitor_filters = esp32_exception_decoder -[env:esp32s3dev_8MB_PSRAM_opi] +[env:esp32s3dev_8MB_opi] ;; ESP32-S3 development board, with 8MB FLASH and >= 8MB PSRAM (memory_type: qio_opi) board = esp32-s3-devkitc-1 ;; generic dev board; the next line adds PSRAM support board_build.arduino.memory_type = qio_opi ;; use with PSRAM: 8MB or 16MB @@ -456,14 +455,34 @@ platform = ${esp32s3.platform} platform_packages = ${esp32s3.platform_packages} upload_speed = 921600 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_PSRAM_opi +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_opi -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 ;-D ARDUINO_USB_CDC_ON_BOOT=0 ;; -D ARDUINO_USB_MODE=1 ;; for boards with serial-to-USB chip -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") - ; -D WLED_RELEASE_NAME=ESP32-S3_PSRAM - -D WLED_USE_PSRAM -DBOARD_HAS_PSRAM ; tells WLED that PSRAM shall be used + -DBOARD_HAS_PSRAM + ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} -board_build.partitions = tools/WLED_ESP32_8MB.csv + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder + +[env:esp32s3_4M_qspi] +;; ESP32-S3, with 4MB FLASH and <= 4MB PSRAM (memory_type: qio_qspi) +board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM +platform = ${esp32s3.platform} +platform_packages = ${esp32s3.platform_packages} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_4M_qspi + -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + -DBOARD_HAS_PSRAM + -D WLED_WATCHDOG_TIMEOUT=0 + ${esp32.AR_build_flags} +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} +board_build.partitions = ${esp32.default_partitions} board_build.f_flash = 80000000L board_build.flash_mode = qio monitor_filters = esp32_exception_decoder @@ -472,17 +491,16 @@ monitor_filters = esp32_exception_decoder platform = ${esp32s2.platform} platform_packages = ${esp32s2.platform_packages} board = lolin_s2_mini -board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv -;board_build.flash_mode = qio -;board_build.f_flash = 80000000L +board_build.partitions = ${esp32.default_partitions} +board_build.flash_mode = qio +board_build.f_flash = 80000000L build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=ESP32-S2 - -DBOARD_HAS_PSRAM -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 + -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this - -D WLED_USE_PSRAM -D WLED_WATCHDOG_TIMEOUT=0 -D CONFIG_ASYNC_TCP_USE_WDT=0 -D LEDPIN=16 @@ -492,4 +510,6 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -D HW_PIN_DATASPI=11 -D HW_PIN_MISOSPI=9 ; -D STATUSLED=15 + ${esp32.AR_build_flags} lib_deps = ${esp32s2.lib_deps} + ${esp32.AR_lib_deps} diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 29f5c6b573..05bd1983e2 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -28,12 +28,15 @@ lib_deps = ${esp8266.lib_deps} ; robtillaart/SHT85@~0.3.3 ; gmag11/QuickESPNow ;@ 0.6.2 ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; https://github.com/kosme/arduinoFFT#develop @ 1.9.2+sha.419d7b0 ;; used for USERMOD_AUDIOREACTIVE - using "known working" hash -; build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +; https://github.com/kosme/arduinoFFT#develop @ 2.0.1 ;; used for USERMOD_AUDIOREACTIVE +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. ; +; Set a release name that may be used to distinguish required binary for flashing +; -D WLED_RELEASE_NAME=ESP32_MULTI_USREMODS +; ; disable specific features ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA @@ -56,6 +59,7 @@ build_flags = ${common.build_flags_esp8266} ; -D IRPIN=4 ; -D RLYPIN=12 ; -D RLYMDE=1 +; -D RLYODRAIN=0 ; -D LED_BUILTIN=2 # GPIO of built-in LED ; ; Limit max buses @@ -155,9 +159,8 @@ build_flags = ${common.build_flags_esp8266} ; set default color order of your led strip ; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; -; use PSRAM if a device (ESP) has one -; -DBOARD_HAS_PSRAM -; -D WLED_USE_PSRAM +; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) +; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue ; ; configure I2C and SPI interface (for various hardware) ; -D I2CSDAPIN=33 # initialise interface @@ -180,7 +183,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini] @@ -190,7 +193,7 @@ platform_packages = ${common.platform_packages} upload_speed = 921600 board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -200,7 +203,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:h803wf] @@ -209,7 +212,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:esp32dev_qio80] @@ -217,7 +220,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32.default_partitions} @@ -232,7 +235,7 @@ board = esp32dev platform = ${esp32_idf_V4.platform} platform_packages = ${esp32_idf_V4.platform_packages} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=ESP32_V4_qio80 #-D WLED_DISABLE_BROWNOUT_DET +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} #-D WLED_DISABLE_BROWNOUT_DET lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.partitions = ${esp32_idf_V4.default_partitions} @@ -241,14 +244,14 @@ board_build.flash_mode = dio [env:esp32s2_saola] board = esp32-s2-saola-1 -platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip -platform_packages = +platform = ${esp32s2.platform} +platform_packages = ${esp32s2.platform_packages} framework = arduino board_build.partitions = tools/WLED_ESP32_4MB_1MB_FS.csv board_build.flash_mode = qio upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32s2.build_flags} #-D WLED_RELEASE_NAME=S2_saola +build_flags = ${common.build_flags} ${esp32s2.build_flags} ;-DLOLIN_WIFI_FIX ;; try this in case Wifi does not work -DARDUINO_USB_CDC_ON_BOOT=1 lib_deps = ${esp32s2.lib_deps} @@ -266,7 +269,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:esp8285_H801] @@ -275,7 +278,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_DISABLE_OTA +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA lib_deps = ${esp8266.lib_deps} [env:d1_mini_5CH_Shojo_PCB] @@ -284,7 +287,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_USE_SHOJO_PCB +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_USE_SHOJO_PCB lib_deps = ${esp8266.lib_deps} [env:d1_mini_debug] @@ -294,7 +297,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} ${common.debug_flags} +build_flags = ${common.build_flags} ${esp8266.build_flags} ${common.debug_flags} lib_deps = ${esp8266.lib_deps} [env:d1_mini_ota] @@ -306,7 +309,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:anavi_miracle_controller] @@ -315,7 +318,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 lib_deps = ${esp8266.lib_deps} [env:esp32c3dev_2MB] @@ -325,7 +328,7 @@ extends = esp32c3 platform = ${esp32c3.platform} platform_packages = ${esp32c3.platform_packages} board = esp32-c3-devkitm-1 -build_flags = ${common.build_flags} ${esp32c3.build_flags} #-D WLED_RELEASE_NAME=ESP32-C3 +build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_WATCHDOG_TIMEOUT=0 -D WLED_DISABLE_OTA ; -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB @@ -342,7 +345,7 @@ platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 460800 build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=16 -D RLYPIN=19 -D BTNPIN=17 @@ -362,7 +365,7 @@ board_build.partitions = ${esp32.default_partitions} [env:m5atom] board = esp32dev build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp32} -D LEDPIN=27 -D BTNPIN=39 +build_flags = ${common.build_flags} ${esp32.build_flags} -D LEDPIN=27 -D BTNPIN=39 lib_deps = ${esp32.lib_deps} platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} @@ -372,14 +375,14 @@ board_build.partitions = ${esp32.default_partitions} board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=1 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=1 lib_deps = ${esp8266.lib_deps} [env:sp511e] board = esp_wroom_02 platform = ${common.platform_wled_default} board_build.ldscript = ${common.ldscript_2m512k} -build_flags = ${common.build_flags_esp8266} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D LEDPIN=3 -D BTNPIN=2 -D IRPIN=5 -D WLED_MAX_BUTTONS=3 lib_deps = ${esp8266.lib_deps} [env:Athom_RGBCW] ;7w and 5w(GU10) bulbs @@ -388,7 +391,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,13,5 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 lib_deps = ${esp8266.lib_deps} @@ -398,7 +401,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=-1 -D RLYPIN=-1 -D DATA_PINS=4,12,14,5,13 -D DEFAULT_LED_TYPE=TYPE_ANALOG_5CH -D WLED_DISABLE_INFRARED -D WLED_MAX_CCT_BLEND=0 -D WLED_USE_IC_CCT lib_deps = ${esp8266.lib_deps} @@ -408,7 +411,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_4Pin_Controller] ; With clock and data interface @@ -417,7 +420,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=12 -D LEDPIN=1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:Athom_5Pin_Controller] ;Analog light strip controller @@ -426,7 +429,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp8266.build_flags} -D BTNPIN=0 -D RLYPIN=-1 DATA_PINS=4,12,14,13 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} [env:MY9291] @@ -435,7 +438,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_DISABLE_OTA -D USERMOD_MY9291 lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -449,7 +452,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} [env:codm-controller-0_6-rev2] @@ -458,7 +461,7 @@ platform = ${common.platform_wled_default} platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags_esp8266} +build_flags = ${common.build_flags} ${esp8266.build_flags} lib_deps = ${esp8266.lib_deps} # ------------------------------------------------------------------------------ @@ -469,7 +472,7 @@ board = esp32dev platform = ${esp32.platform} platform_packages = ${esp32.platform_packages} upload_speed = 921600 -build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_DISABLE_BROWNOUT_DET -D WLED_DISABLE_INFRARED -D USERMOD_RTC -D USERMOD_ELEKSTUBE_IPS -D LEDPIN=12 diff --git a/requirements.txt b/requirements.txt index 1c0644f98e..d6f86e2024 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ marshmallow==3.19.0 # via platformio packaging==23.1 # via marshmallow -platformio==6.1.6 +platformio==6.1.14 # via -r requirements.in pyelftools==0.29 # via platformio diff --git a/tools/WLED_ESP32-wrover_4MB.csv b/tools/WLED_ESP32-wrover_4MB.csv index a179a89d0e..39c88e5437 100644 --- a/tools/WLED_ESP32-wrover_4MB.csv +++ b/tools/WLED_ESP32-wrover_4MB.csv @@ -1,6 +1,6 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x180000, -app1, app, ota_1, 0x190000,0x180000, -spiffs, data, spiffs, 0x310000,0xF0000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, diff --git a/tools/WLED_ESP32_4MB_256KB_FS.csv b/tools/WLED_ESP32_4MB_256KB_FS.csv new file mode 100644 index 0000000000..e54c22bbdc --- /dev/null +++ b/tools/WLED_ESP32_4MB_256KB_FS.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1D0000, +app1, app, ota_1, 0x1E0000,0x1D0000, +spiffs, data, spiffs, 0x3B0000,0x40000, +coredump, data, coredump,,64K \ No newline at end of file diff --git a/tools/WLED_ESP32_4MB_700k_FS.csv b/tools/WLED_ESP32_4MB_700k_FS.csv new file mode 100644 index 0000000000..39c88e5437 --- /dev/null +++ b/tools/WLED_ESP32_4MB_700k_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1A0000, +app1, app, ota_1, 0x1B0000,0x1A0000, +spiffs, data, spiffs, 0x350000,0xB0000, diff --git a/tools/cdata-test.js b/tools/cdata-test.js index 55f068073a..6f27fb717f 100644 --- a/tools/cdata-test.js +++ b/tools/cdata-test.js @@ -83,6 +83,7 @@ describe('Script', () => { // Backup files fs.cpSync("wled00/data", "wled00Backup", { recursive: true }); fs.cpSync("tools/cdata.js", "cdata.bak.js"); + fs.cpSync("package.json", "package.bak.json"); }); after(() => { // Restore backup @@ -90,6 +91,8 @@ describe('Script', () => { fs.renameSync("wled00Backup", "wled00/data"); fs.rmSync("tools/cdata.js"); fs.renameSync("cdata.bak.js", "tools/cdata.js"); + fs.rmSync("package.json"); + fs.renameSync("package.bak.json", "package.json"); }); // delete all html_*.h files @@ -131,7 +134,7 @@ describe('Script', () => { // run script cdata.js again and wait for it to finish await execPromise('node tools/cdata.js'); - checkIfFileWasNewlyCreated(path.join(folderPath, resultFile)); + await checkIfFileWasNewlyCreated(path.join(folderPath, resultFile)); } describe('should build if', () => { @@ -182,6 +185,10 @@ describe('Script', () => { it('cdata.js changes', async () => { await testFileModification('tools/cdata.js', 'html_ui.h'); }); + + it('package.json changes', async () => { + await testFileModification('package.json', 'html_ui.h'); + }); }); describe('should not build if', () => { diff --git a/tools/cdata.js b/tools/cdata.js index 16475d8110..12dda1cbe0 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -2,7 +2,7 @@ * Writes compressed C arrays of data files (web interface) * How to use it? * - * 1) Install Node 11+ and npm + * 1) Install Node 20+ and npm * 2) npm install * 3) npm run build * @@ -15,10 +15,10 @@ * It uses NodeJS packages to inline, minify and GZIP files. See writeHtmlGzipped and writeChunks invocations at the bottom of the page. */ -const fs = require("fs"); +const fs = require("node:fs"); const path = require("path"); const inliner = require("inliner"); -const zlib = require("zlib"); +const zlib = require("node:zlib"); const CleanCSS = require("clean-css"); const minifyHtml = require("html-minifier-terser").minify; const packageJson = require("../package.json"); @@ -207,7 +207,7 @@ function isAnyFileInFolderNewerThan(folderPath, time) { } // Check if the web UI is already built -function isAlreadyBuilt(folderPath) { +function isAlreadyBuilt(webUIPath, packageJsonPath = "package.json") { let lastBuildTime = Infinity; for (const file of output) { @@ -220,7 +220,7 @@ function isAlreadyBuilt(folderPath) { } } - return !isAnyFileInFolderNewerThan(folderPath, lastBuildTime) && !isFileNewerThan("tools/cdata.js", lastBuildTime); + return !isAnyFileInFolderNewerThan(webUIPath, lastBuildTime) && !isFileNewerThan(packageJsonPath, lastBuildTime) && !isFileNewerThan(__filename, lastBuildTime); } // Don't run this script if we're in a test environment diff --git a/usermods/BH1750_v2/usermod_bh1750.h b/usermods/BH1750_v2/usermod_bh1750.h index ede4aabc48..2a2bd46370 100644 --- a/usermods/BH1750_v2/usermod_bh1750.h +++ b/usermods/BH1750_v2/usermod_bh1750.h @@ -59,7 +59,7 @@ class Usermod_BH1750 : public Usermod bool sensorFound = false; // Home Assistant and MQTT - String mqttLuminanceTopic = F(""); + String mqttLuminanceTopic; bool mqttInitialized = false; bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md new file mode 100644 index 0000000000..958e6def2e --- /dev/null +++ b/usermods/MAX17048_v2/readme.md @@ -0,0 +1,64 @@ +# Adafruit MAX17048 Usermod (LiPo & LiIon Battery Monitor & Fuel Gauge) +This usermod reads information from an Adafruit MAX17048 and outputs the following: +- Battery Voltage +- Battery Level Percentage + + +## Dependencies +Libraries: +- `Adafruit_BusIO@~1.14.5` (by [adafruit](https://github.com/adafruit/Adafruit_BusIO)) +- `Adafruit_MAX1704X@~1.0.2` (by [adafruit](https://github.com/adafruit/Adafruit_MAX1704X)) + +These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). +Data is published over MQTT - make sure you've enabled the MQTT sync interface. + +## Compilation + +To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: +```ini +[env:usermod_max17048_d1_mini] +extends = env:d1_mini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_MAX17048 +lib_deps = + ${esp8266.lib_deps} + https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 + https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 +``` + +### Configuration Options +The following settings can be set at compile-time but are configurable on the usermod menu (except First Monitor time): +- USERMOD_MAX17048_MIN_MONITOR_INTERVAL (the min number of milliseconds between checks, defaults to 10,000 ms) +- USERMOD_MAX17048_MAX_MONITOR_INTERVAL (the max number of milliseconds between checks, defaults to 10,000 ms) +- USERMOD_MAX17048_FIRST_MONITOR_AT + + +Additionally, the Usermod Menu allows you to: +- Enable or Disable the usermod +- Enable or Disable Home Assistant Discovery (turn on/off to sent MQTT Discovery entries for Home Assistant) +- Configure SCL/SDA GPIO Pins + +## API +The following method is available to interact with the usermod from other code modules: +- `getBatteryVoltageV` read the last battery voltage (in Volt) obtained from the sensor +- `getBatteryPercent` reads the last battery percentage obtained from the sensor + +## MQTT +MQTT topics are as follows (`` is set in MQTT section of Sync Setup menu): +Measurement type | MQTT topic +--- | --- +Battery Voltage | `/batteryVoltage` +Battery Percent | `/batteryPercent` + +## Authors +Carlos Cruz [@ccruz09](https://github.com/ccruz09) + + +## Revision History +Jan 2024 +- Added Home Assistant Discovery +- Implemented PinManager to register pins +- Added API call for other modules to read battery voltage and percentage +- Added info-screen outputs +- Updated `readme.md` \ No newline at end of file diff --git a/usermods/MAX17048_v2/usermod_max17048.h b/usermods/MAX17048_v2/usermod_max17048.h new file mode 100644 index 0000000000..c3a2664ab1 --- /dev/null +++ b/usermods/MAX17048_v2/usermod_max17048.h @@ -0,0 +1,281 @@ +// force the compiler to show a warning to confirm that this file is included +#warning **** Included USERMOD_MAX17048 V2.0 **** + +#pragma once + +#include "wled.h" +#include "Adafruit_MAX1704X.h" + + +// the max interval to check battery level, 10 seconds +#ifndef USERMOD_MAX17048_MAX_MONITOR_INTERVAL +#define USERMOD_MAX17048_MAX_MONITOR_INTERVAL 10000 +#endif + +// the min interval to check battery level, 500 ms +#ifndef USERMOD_MAX17048_MIN_MONITOR_INTERVAL +#define USERMOD_MAX17048_MIN_MONITOR_INTERVAL 500 +#endif + +// how many seconds after boot to perform the first check, 10 seconds +#ifndef USERMOD_MAX17048_FIRST_MONITOR_AT +#define USERMOD_MAX17048_FIRST_MONITOR_AT 10000 +#endif + +/* + * Usermod to display Battery Life using Adafruit's MAX17048 LiPoly/ LiIon Fuel Gauge and Battery Monitor. + */ +class Usermod_MAX17048 : public Usermod { + + private: + + bool enabled = true; + + unsigned long maxReadingInterval = USERMOD_MAX17048_MAX_MONITOR_INTERVAL; + unsigned long minReadingInterval = USERMOD_MAX17048_MIN_MONITOR_INTERVAL; + unsigned long lastCheck = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); + + + uint8_t VoltageDecimals = 3; // Number of decimal places in published voltage values + uint8_t PercentDecimals = 1; // Number of decimal places in published percent values + + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + static const char _maxReadInterval[]; + static const char _minReadInterval[]; + static const char _HomeAssistantDiscovery[]; + + bool monitorFound = false; + bool firstReadComplete = false; + bool initDone = false; + + Adafruit_MAX17048 maxLipo; + float lastBattVoltage = -10; + float lastBattPercent = -1; + + // MQTT and Home Assistant Variables + bool HomeAssistantDiscovery = false; // Publish Home Assistant Device Information + bool mqttInitialized = false; + + void _mqttInitialize() + { + char mqttBatteryVoltageTopic[128]; + char mqttBatteryPercentTopic[128]; + + snprintf_P(mqttBatteryVoltageTopic, 127, PSTR("%s/batteryVoltage"), mqttDeviceTopic); + snprintf_P(mqttBatteryPercentTopic, 127, PSTR("%s/batteryPercent"), mqttDeviceTopic); + + if (HomeAssistantDiscovery) { + _createMqttSensor(F("BatteryVoltage"), mqttBatteryVoltageTopic, "voltage", F("V")); + _createMqttSensor(F("BatteryPercent"), mqttBatteryPercentTopic, "battery", F("%")); + } + } + + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + " " + name; + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + void publishMqtt(const char *topic, const char* state) { + #ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash the 8266 + if (WLED_MQTT_CONNECTED){ + char subuf[128]; + snprintf_P(subuf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(subuf, 0, false, state); + } + #endif + } + + public: + + inline void enable(bool enable) { enabled = enable; } + + inline bool isEnabled() { return enabled; } + + void setup() { + // do your set-up here + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + monitorFound = maxLipo.begin(); + initDone = true; + } + + void loop() { + // if usermod is disabled or called during strip updating just exit + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + + unsigned long now = millis(); + + if (now - lastCheck < minReadingInterval) { return; } + + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float battVoltage = maxLipo.cellVoltage(); + float battPercent = maxLipo.cellPercent(); + lastCheck = millis(); + firstReadComplete = true; + + if (shouldUpdate) + { + lastBattVoltage = roundf(battVoltage * powf(10, VoltageDecimals)) / powf(10, VoltageDecimals); + lastBattPercent = roundf(battPercent * powf(10, PercentDecimals)) / powf(10, PercentDecimals); + lastSend = millis(); + + publishMqtt("batteryVoltage", String(lastBattVoltage, VoltageDecimals).c_str()); + publishMqtt("batteryPercent", String(lastBattPercent, PercentDecimals).c_str()); + DEBUG_PRINTLN(F("Battery Voltage: ") + String(lastBattVoltage, VoltageDecimals) + F("V")); + DEBUG_PRINTLN(F("Battery Percent: ") + String(lastBattPercent, PercentDecimals) + F("%")); + } + } + + void onMqttConnect(bool sessionPresent) + { + if (WLED_MQTT_CONNECTED && !mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + } + + inline float getBatteryVoltageV() { + return (float) lastBattVoltage; + } + + inline float getBatteryPercent() { + return (float) lastBattPercent; + } + + void addToJsonInfo(JsonObject& root) + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + + JsonArray battery_json = user.createNestedArray(F("Battery Monitor")); + if (!enabled) { + battery_json.add(F("Disabled")); + } + else if(!monitorFound) { + battery_json.add(F("MAX17048 Not Found")); + } + else if (!firstReadComplete) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + battery_json.add((USERMOD_MAX17048_FIRST_MONITOR_AT - millis()) / 1000); + battery_json.add(F(" sec until read")); + } else { + battery_json.add(F("Enabled")); + JsonArray voltage_json = user.createNestedArray(F("Battery Voltage")); + voltage_json.add(lastBattVoltage); + voltage_json.add(F("V")); + JsonArray percent_json = user.createNestedArray(F("Battery Percent")); + percent_json.add(lastBattPercent); + percent_json.add(F("%")); + } + } + + void addToJsonState(JsonObject& root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) + { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod[FPSTR(_enabled)] = enabled; + } + + void readFromJsonState(JsonObject& root) + { + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) + { + if (usermod[FPSTR(_enabled)].is()) + { + enabled = usermod[FPSTR(_enabled)].as(); + } + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; + DEBUG_PRINT(F(_name)); + DEBUG_PRINTLN(F(" config saved.")); + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + DEBUG_PRINT(F(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, USERMOD_MAX17048_MAX_MONITOR_INTERVAL); + configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, USERMOD_MAX17048_MIN_MONITOR_INTERVAL); + configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + // first run: reading from cfg.json + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); + // changing parameters from settings page + } + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_MAX17048; + } + +}; + + +// add more strings here to reduce flash memory usage +const char Usermod_MAX17048::_name[] PROGMEM = "Adafruit MAX17048 Battery Monitor"; +const char Usermod_MAX17048::_enabled[] PROGMEM = "enabled"; +const char Usermod_MAX17048::_maxReadInterval[] PROGMEM = "max-read-interval-ms"; +const char Usermod_MAX17048::_minReadInterval[] PROGMEM = "min-read-interval-ms"; +const char Usermod_MAX17048::_HomeAssistantDiscovery[] PROGMEM = "HomeAssistantDiscovery"; diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h new file mode 100644 index 0000000000..af3f5bcf02 --- /dev/null +++ b/usermods/TetrisAI_v2/gridbw.h @@ -0,0 +1,117 @@ +/****************************************************************************** + * @file : gridbw.h + * @brief : contains the tetris grid as binary so black and white version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDBW_H__ +#define __GRIDBW_H__ + +#include +#include +#include "pieces.h" + +using namespace std; + +class GridBW +{ +private: +public: + uint8_t width; + uint8_t height; + std::vector pixels; + + GridBW(uint8_t width, uint8_t height): + width(width), + height(height), + pixels(height) + { + if (width > 32) + { + throw std::invalid_argument("maximal width is 32"); + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width); + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width); + } + } + + bool noCollision(Piece* piece, uint8_t x, uint8_t y) + { + //if it touches a wall it is a collision + if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height) + { + return false; + } + + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))]) + { + return false; + } + } + return true; + } + + void findLandingPosition(Piece* piece) + { + // move down until the piece bumps into some occupied pixels or the 'wall' + while (noCollision(piece, piece->x, piece->landingY)) + { + piece->landingY++; + } + + //at this point the positon is 'in the wall' or 'over some occupied pixel' + //so the previous position was the last correct one (clamped to 0 as minimum). + piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0; + } + + void cleanupFullLines() + { + uint8_t offset = 0; + + //from "height - 1" to "0", so from bottom row to top + for (uint8_t row = height; row-- > 0; ) + { + //full line? + if (isLineFull(row)) + { + offset++; + pixels[row] = 0x0; + continue; + } + + if (offset > 0) + { + pixels[row + offset] = pixels[row]; + pixels[row] = 0x0; + } + } + } + + bool isLineFull(uint8_t y) + { + return pixels[y] == (uint32_t)((1 << width) - 1); + } +}; + +#endif /* __GRIDBW_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridcolor.h b/usermods/TetrisAI_v2/gridcolor.h new file mode 100644 index 0000000000..5c5ce7e422 --- /dev/null +++ b/usermods/TetrisAI_v2/gridcolor.h @@ -0,0 +1,132 @@ +/****************************************************************************** + * @file : gridcolor.h + * @brief : contains the tetris grid as 8bit indexed color version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDCOLOR_H__ +#define __GRIDCOLOR_H__ +#include +#include +#include +#include "gridbw.h" +#include "gridcolor.h" + +using namespace std; + +class GridColor +{ +private: +public: + uint8_t width; + uint8_t height; + GridBW gridBW; + std::vector pixels; + + GridColor(uint8_t width, uint8_t height): + width(width), + height(height), + gridBW(width, height), + pixels(width* height) + {} + + void clear() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + *getPixel(x, y) = 0; + } + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex; + } + } + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = 0; + } + } + } + } + + void cleanupFullLines() + { + uint8_t offset = 0; + //from "height - 1" to "0", so from bottom row to top + for (uint8_t y = height; y-- > 0; ) + { + if (gridBW.isLineFull(y)) + { + offset++; + for (uint8_t x = 0; x < width; x++) + { + pixels[y * width + x] = 0; + } + continue; + } + + if (offset > 0) + { + if (gridBW.pixels[y]) + { + for (uint8_t x = 0; x < width; x++) + { + pixels[(y + offset) * width + x] = pixels[y * width + x]; + pixels[y * width + x] = 0; + } + } + } + } + gridBW.cleanupFullLines(); + } + + uint8_t* getPixel(uint8_t x, uint8_t y) + { + return &pixels[y * width + x]; + } + + void sync() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + gridBW.pixels[y] <<= 1; + if (*getPixel(x, y) != 0) + { + gridBW.pixels[y] |= 0x1; + } + } + } + } +}; + +#endif /* __GRIDCOLOR_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/pieces.h b/usermods/TetrisAI_v2/pieces.h new file mode 100644 index 0000000000..5d461615ae --- /dev/null +++ b/usermods/TetrisAI_v2/pieces.h @@ -0,0 +1,184 @@ +/****************************************************************************** + * @file : pieces.h + * @brief : contains the tetris pieces with their colors indecies + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __PIECES_H__ +#define __PIECES_H__ + +#include +#include + +#include +#include +#include +#include + +#define numPieces 7 + +struct PieceRotation +{ + uint8_t width; + uint8_t height; + uint16_t rows; +}; + +struct PieceData +{ + uint8_t rotCount; + uint8_t colorIndex; + PieceRotation rotations[4]; +}; + +PieceData piecesData[numPieces] = { + // I + { + 2, + 1, + { + { 1, 4, 0b0001000100010001}, + { 4, 1, 0b0000000000001111} + } + }, + // O + { + 1, + 2, + { + { 2, 2, 0b0000000000110011} + } + }, + // Z + { + 2, + 3, + { + { 3, 2, 0b0000000001100011}, + { 2, 3, 0b0000000100110010} + } + }, + // S + { + 2, + 4, + { + { 3, 2, 0b0000000000110110}, + { 2, 3, 0b0000001000110001} + } + }, + // L + { + 4, + 5, + { + { 2, 3, 0b0000001000100011}, + { 3, 2, 0b0000000001110100}, + { 2, 3, 0b0000001100010001}, + { 3, 2, 0b0000000000010111} + } + }, + // J + { + 4, + 6, + { + { 2, 3, 0b0000000100010011}, + { 3, 2, 0b0000000001000111}, + { 2, 3, 0b0000001100100010}, + { 3, 2, 0b0000000001110001} + } + }, + // T + { + 4, + 7, + { + { 3, 2, 0b0000000001110010}, + { 2, 3, 0b0000000100110001}, + { 3, 2, 0b0000000000100111}, + { 2, 3, 0b0000001000110010} + } + }, +}; + +class Piece +{ +private: +public: + uint8_t x; + uint8_t y; + PieceData* pieceData; + uint8_t rotation; + uint8_t landingY; + + Piece(uint8_t pieceIndex = 0): + x(0), + y(0), + rotation(0), + landingY(0) + { + this->pieceData = &piecesData[pieceIndex]; + } + + void reset() + { + this->rotation = 0; + this->x = 0; + this->y = 0; + this->landingY = 0; + } + + uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width) + { + if (x < width) + { + //shift the row with the "top-left" position to the "x" position + auto shiftx = (width - 1) - x; + auto topleftx = (getRotation().width - 1); + + auto finalShift = shiftx - topleftx; + auto row = getRow(y); + auto finalResult = row << finalShift; + + return finalResult; + } + return 0xffffffff; + } + + uint8_t getRow(uint8_t y) + { + if (y < 4) + { + return (getRotation().rows >> (12 - (4 * y))) & 0xf; + } + return 0xf; + } + + bool getPixel(uint8_t x, uint8_t y) + { + if(x > getRotation().width - 1 || y > getRotation().height - 1 ) + { + return false; + } + + if (x < 4 && y < 4) + { + return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1; + } + return false; + } + + PieceRotation getRotation() + { + return this->pieceData->rotations[rotation]; + } +}; + +#endif /* __PIECES_H__ */ diff --git a/usermods/TetrisAI_v2/rating.h b/usermods/TetrisAI_v2/rating.h new file mode 100644 index 0000000000..4504ff468b --- /dev/null +++ b/usermods/TetrisAI_v2/rating.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * @file : rating.h + * @brief : contains the tetris rating of a grid + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __RATING_H__ +#define __RATING_H__ + +#include +#include +#include +#include +#include +#include "rating.h" + +using namespace std; + +class Rating +{ +private: +public: + uint8_t minHeight; + uint8_t maxHeight; + uint16_t holes; + uint8_t fullLines; + uint16_t bumpiness; + uint16_t aggregatedHeight; + double score; + uint8_t width; + std::vector lineHights; + + Rating(uint8_t width): + width(width), + lineHights(width) + { + reset(); + } + + void reset() + { + this->minHeight = 0; + this->maxHeight = 0; + + for (uint8_t line = 0; line < this->width; line++) + { + this->lineHights[line] = 0; + } + + this->holes = 0; + this->fullLines = 0; + this->bumpiness = 0; + this->aggregatedHeight = 0; + this->score = -DBL_MAX; + } +}; + +#endif /* __RATING_H__ */ diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md new file mode 100644 index 0000000000..b21bc5fde1 --- /dev/null +++ b/usermods/TetrisAI_v2/readme.md @@ -0,0 +1,33 @@ +# Tetris AI effect usermod + +This usermod brings you a effect brings a self playing Tetris game. The mod needs version 0.14 or above as it is based on matrix support. The effect was tested on an ESP32 with a WS2812B 16x16 matrix. + +Version 1.0 + +## Installation + +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. + +## Usage + +It is best to set the background color to black, the border color to light grey and the game over color (foreground) to dark grey. + +### Sliders and boxes + +#### Sliders + +* speed: speed the game plays +* look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2) +* intelligence: how good the AI will play +* Rotate color: make the colors shift (rotate) every few cicles +* Mistakes free: how many good moves between mistakes (if activated) + +#### Checkboxes + +* show next: if true a space of 5 pixels from the right is used to show the next pieces. The whole segment is used for the grid otherwise. +* show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces +* mistakes: if true the worst instead of the best move is choosen every few moves (read above) + +## Best results + + If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party. \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h new file mode 100644 index 0000000000..ef7868a47c --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -0,0 +1,302 @@ +/****************************************************************************** + * @file : ai.h + * @brief : contains the heuristic + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __AI_H__ +#define __AI_H__ + +#include "gridbw.h" +#include "rating.h" + +using namespace std; + +class TetrisAI +{ +private: +public: + double aHeight; + double fullLines; + double holes; + double bumpiness; + bool findWorstMove = false; + + uint8_t countOnes(uint32_t vector) + { + uint8_t count = 0; + while (vector) + { + vector &= (vector - 1); + count++; + } + return count; + } + + void updateRating(GridBW grid, Rating* rating) + { + rating->minHeight = 0; + rating->maxHeight = 0; + rating->holes = 0; + rating->fullLines = 0; + rating->bumpiness = 0; + rating->aggregatedHeight = 0; + fill(rating->lineHights.begin(), rating->lineHights.end(), 0); + + uint32_t columnvector = 0x0; + uint32_t lastcolumnvector = 0x0; + for (uint8_t row = 0; row < grid.height; row++) + { + columnvector |= grid.pixels[row]; + + //first (highest) column makes it + if (rating->maxHeight == 0 && columnvector) + { + rating->maxHeight = grid.height - row; + } + + //if column vector is full we found the minimal height (or it stays zero) + if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1))) + { + rating->minHeight = grid.height - row; + } + + //line full if all ones in mask :-) + if (grid.isLineFull(row)) + { + rating->fullLines++; + } + + //holes are basically a XOR with the "full" columns + rating->holes += countOnes(columnvector ^ grid.pixels[row]); + + //calculate the difference (XOR) between the current column vector and the last one + uint32_t columnDelta = columnvector ^ lastcolumnvector; + + //process every new column + uint8_t index = 0; + while (columnDelta) + { + //if this is a new column + if (columnDelta & 0x1) + { + //update hight of this column + rating->lineHights[(grid.width - 1) - index] = grid.height - row; + + // update aggregatedHeight + rating->aggregatedHeight += grid.height - row; + } + index++; + columnDelta >>= 1; + } + lastcolumnvector = columnvector; + } + + //compare every two columns to get the difference and add them up + for (uint8_t column = 1; column < grid.width; column++) + { + rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]); + } + + rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness)); + } + + TetrisAI(): TetrisAI(-0.510066, 0.760666, -0.35663, -0.184483) + {} + + TetrisAI(double aHeight, double fullLines, double holes, double bumpiness): + aHeight(aHeight), + fullLines(fullLines), + holes(holes), + bumpiness(bumpiness) + {} + + void findBestMove(GridBW grid, Piece *piece) + { + vector pieces = {*piece}; + findBestMove(grid, &pieces); + *piece = pieces[0]; + } + + void findBestMove(GridBW grid, std::vector *pieces) + { + findBestMove(grid, pieces->begin(), pieces->end()); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end) + { + Rating bestRating(grid.width); + findBestMove(grid, start, end, &bestRating); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + grid.cleanupFullLines(); + Rating curRating(grid.width); + Rating deeperRating(grid.width); + Piece piece = *start; + + // for every rotation of the piece + for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++) + { + // put piece to top left corner + piece.x = 0; + piece.y = 0; + + //test for every column + for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++) + { + //todo optimise by the use of the previous grids height + piece.landingY = 0; + //will set landingY to final position + grid.findLandingPosition(&piece); + + // draw piece + grid.placePiece(&piece, piece.x, piece.landingY); + + if(start == end - 1) + { + //at the deepest level + updateRating(grid, &curRating); + } + else + { + //go deeper to take another piece into account + findBestMove(grid, start + 1, end, &deeperRating); + curRating = deeperRating; + } + + // eraese piece + grid.erasePiece(&piece, piece.x, piece.landingY); + + if(findWorstMove) + { + //init rating for worst + if(bestRating->score == -DBL_MAX) + { + bestRating->score = DBL_MAX; + } + + // update if we found a worse one + if (bestRating->score > curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + else + { + // update if we found a better one + if (bestRating->score < curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + } + } + } + + bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + //vector with pieces + //for every piece + //for every + switch (expression) + { + case INIT: + break; + + default: + break; + } + } + + bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + //INIT + grid.cleanupFullLines(); + Rating curRating(grid.width); + Rating deeperRating(grid.width); + Piece piece = *start; + + // for every rotation of the piece + piece.rotation = 0; + + //HANDLE + while (piece.rotation < piece.pieceData->rotCount) + { + // put piece to top left corner + piece.x = 0; + piece.y = 0; + + //test for every column + piece.x = 0; + while (piece.x <= grid.width - piece.getRotation().width) + { + + //todo optimise by the use of the previous grids height + piece.landingY = 0; + //will set landingY to final position + grid.findLandingPosition(&piece); + + // draw piece + grid.placePiece(&piece, piece.x, piece.landingY); + + if(start == end - 1) + { + //at the deepest level + updateRating(grid, &curRating); + } + else + { + //go deeper to take another piece into account + findBestMove(grid, start + 1, end, &deeperRating); + curRating = deeperRating; + } + + // eraese piece + grid.erasePiece(&piece, piece.x, piece.landingY); + + if(findWorstMove) + { + //init rating for worst + if(bestRating->score == -DBL_MAX) + { + bestRating->score = DBL_MAX; + } + + // update if we found a worse one + if (bestRating->score > curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + else + { + // update if we found a better one + if (bestRating->score < curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + piece.x++; + } + piece.rotation++; + } + + //EXIT + + return true; + } +}; + +#endif /* __AI_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisaigame.h b/usermods/TetrisAI_v2/tetrisaigame.h new file mode 100644 index 0000000000..de3c86e7e3 --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisaigame.h @@ -0,0 +1,150 @@ +/****************************************************************************** + * @file : tetrisaigame.h + * @brief : main tetris functions + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISAIGAME_H__ +#define __TETRISAIGAME_H__ + +#include +#include +#include +#include "pieces.h" +#include "gridcolor.h" +#include "tetrisbag.h" +#include "tetrisai.h" + +using namespace std; + +class TetrisAIGame +{ +private: + bool animateFallOfPiece(Piece* piece, bool skip) + { + if (skip || piece->y >= piece->landingY) + { + piece->y = piece->landingY; + grid.gridBW.placePiece(piece, piece->x, piece->landingY); + grid.placePiece(piece, piece->x, piece->y); + return false; + } + else + { + // eraese last drawing + grid.erasePiece(piece, piece->x, piece->y); + + //move piece down + piece->y++; + + // draw piece + grid.placePiece(piece, piece->x, piece->y); + + return true; + } + } + +public: + uint8_t width; + uint8_t height; + uint8_t nLookAhead; + TetrisBag bag; + GridColor grid; + TetrisAI ai; + Piece curPiece; + PieceData* piecesData; + enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT; + + TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces): + width(width), + height(height), + nLookAhead(nLookAhead), + bag(nPieces, 1, nLookAhead), + grid(width, height + 4), + ai(), + piecesData(piecesData) + { + } + + void nextPiece() + { + grid.cleanupFullLines(); + bag.queuePiece(); + } + + void findBestMove() + { + ai.findBestMove(grid.gridBW, &bag.piecesQueue); + } + + bool animateFall(bool skip) + { + return animateFallOfPiece(&(bag.piecesQueue[0]), skip); + } + + bool isGameOver() + { + //if there is something in the 4 lines of the hidden area the game is over + return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3]; + } + + void poll() + { + switch (state) + { + case INIT: + reset(); + state = TEST_GAME_OVER; + break; + case TEST_GAME_OVER: + if (isGameOver()) + { + state = ANIMATE_GAME_OVER; + } + else + { + state = GET_NEXT_PIECE; + } + break; + case GET_NEXT_PIECE: + nextPiece(); + state = FIND_BEST_MOVE; + break; + case FIND_BEST_MOVE: + findBestMove(); + state = ANIMATE_MOVE; + break; + case ANIMATE_MOVE: + if (!animateFall(false)) + { + state = TEST_GAME_OVER; + } + break; + case ANIMATE_GAME_OVER: + static auto curPixel = grid.pixels.size(); + grid.pixels[curPixel] = 254; + + if (curPixel == 0) + { + state = INIT; + curPixel = grid.pixels.size(); + } + curPixel--; + break; + } + } + + void reset() + { + grid.clear(); + bag.init(); + } +}; + +#endif /* __TETRISAIGAME_H__ */ diff --git a/usermods/TetrisAI_v2/tetrisbag.h b/usermods/TetrisAI_v2/tetrisbag.h new file mode 100644 index 0000000000..3ecadbc0be --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisbag.h @@ -0,0 +1,100 @@ +/****************************************************************************** + * @file : tetrisbag.h + * @brief : the tetris implementation of a random piece generator + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISBAG_H__ +#define __TETRISBAG_H__ + +#include +#include +#include + +#include "tetrisbag.h" + +class TetrisBag +{ +private: +public: + uint8_t nPieces; + uint8_t nBagLength; + uint8_t bagIdx; + std::vector bag; + std::vector piecesQueue; + + TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength): + nPieces(nPieces), + nBagLength(nBagLength), + bag(nPieces * nBagLength), + piecesQueue(queueLength) + { + init(); + } + + void init() + { + //will shuffle the bag at first use + bagIdx = nPieces - 1; + + for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++) + { + bag[bagIndex] = bagIndex % nPieces; + } + + //will init the queue + for (uint8_t index = 0; index < piecesQueue.size(); index++) + { + queuePiece(); + } + } + + void shuffleBag() + { + uint8_t temp; + uint8_t swapIdx; + for (int index = nPieces - 1; index > 0; index--) + { + //get candidate to swap + swapIdx = rand() % index; + + //swap it! + temp = bag[swapIdx]; + bag[swapIdx] = bag[index]; + bag[index] = temp; + } + } + + Piece getNextPiece() + { + bagIdx++; + if (bagIdx >= nPieces) + { + shuffleBag(); + bagIdx = 0; + } + return Piece(bag[bagIdx]); + } + + void queuePiece() + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = getNextPiece(); + } + + void queuePiece(uint8_t idx) + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces); + } +}; + +#endif /* __TETRISBAG_H__ */ diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h new file mode 100644 index 0000000000..1c077d0485 --- /dev/null +++ b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h @@ -0,0 +1,222 @@ +#pragma once + +#include "wled.h" +#include "FX.h" +#include "fcn_declare.h" + +#include "tetrisaigame.h" +// By: muebau + +typedef struct TetrisAI_data +{ + unsigned long lastTime = 0; + TetrisAIGame tetris; + uint8_t intelligence; + uint8_t rotate; + bool showNext; + bool showBorder; + uint8_t colorOffset; + uint8_t colorInc; + uint8_t mistaceCountdown; +} tetrisai_data; + +void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) +{ + SEGMENT.fill(SEGCOLOR(1)); + + //GRID + for (auto index_y = 4; index_y < tetris->grid.height; index_y++) + { + for (auto index_x = 0; index_x < tetris->grid.width; index_x++) + { + CRGB color; + if (*tetris->grid.getPixel(index_x, index_y) == 0) + { + //BG color + color = SEGCOLOR(1); + } + //game over animation + else if(*tetris->grid.getPixel(index_x, index_y) == 254) + { + //use fg + color = SEGCOLOR(0); + } + else + { + //spread the color over the whole palette + uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32; + colorIndex += tetrisai_data->colorOffset; + color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); + } + + SEGMENT.setPixelColorXY(index_x, index_y - 4, color); + } + } + tetrisai_data->colorOffset += tetrisai_data->colorInc; + + //NEXT PIECE AREA + if (tetrisai_data->showNext) + { + //BORDER + if (tetrisai_data->showBorder) + { + //draw a line 6 pixels from right with the border color + for (auto index_y = 0; index_y < SEGMENT.virtualHeight(); index_y++) + { + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() - 6, index_y, SEGCOLOR(2)); + } + } + + //NEXT PIECE + int piecesOffsetX = SEGMENT.virtualWidth() - 4; + int piecesOffsetY = 1; + for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++) + { + uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5; + + Piece piece(tetris->bag.piecesQueue[nextPieceIdx]); + + for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++) + { + if (piece.getPixel(pieceX, pieceY)) + { + uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset); + SEGMENT.setPixelColorXY(piecesOffsetX + pieceX, piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); + } + } + } + } + } +} + +//////////////////////////// +// 2D Tetris AI // +//////////////////////////// +uint16_t mode_2DTetrisAI() +{ + if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) + { + // not a 2D set-up + SEGMENT.fill(SEGCOLOR(0)); + return 350; + } + TetrisAI_data* tetrisai_data = reinterpret_cast(SEGENV.data); + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + //range 0 - 1024ms => 1024/255 ~ 4 + uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed); + int16_t msDelayGameOver = msDelayMove / 4; + + //range 0 - 2 (not including current) + uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1; + //range 0 - 16 + tetrisai_data->colorInc = SEGMENT.custom2 >> 4; + + if (!tetrisai_data->tetris || (tetrisai_data->tetris.nLookAhead != nLookAhead + || tetrisai_data->showNext != SEGMENT.check1 + || tetrisai_data->showBorder != SEGMENT.check2 + ) + ) + { + tetrisai_data->showNext = SEGMENT.check1; + tetrisai_data->showBorder = SEGMENT.check2; + + //not more than 32 as this is the limit of this implementation + uint8_t gridWidth = cols < 32 ? cols : 32; + uint8_t gridHeight = rows; + + // do we need space for the 'next' section? + if (tetrisai_data->showNext) + { + // make space for the piece and one pixel of space + gridWidth = gridWidth - 5; + + // do we need space for a border? + if (tetrisai_data->showBorder) + { + gridWidth = gridWidth - 1; + } + } + + tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); + SEGMENT.fill(SEGCOLOR(1)); + } + + if (tetrisai_data->intelligence != SEGMENT.custom1) + { + tetrisai_data->intelligence = SEGMENT.custom1; + double dui = 0.2 - (0.2 * (tetrisai_data->intelligence / 255.0)); + + tetrisai_data->tetris.ai.aHeight = -0.510066 + dui; + tetrisai_data->tetris.ai.fullLines = 0.760666 - dui; + tetrisai_data->tetris.ai.holes = -0.35663 + dui; + tetrisai_data->tetris.ai.bumpiness = -0.184483 + dui; + } + + if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) + { + if (millis() - tetrisai_data->lastTime > msDelayMove) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = millis(); + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) + { + if (millis() - tetrisai_data->lastTime > msDelayGameOver) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = millis(); + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE) + { + if (SEGMENT.check3) + { + if(tetrisai_data->mistaceCountdown == 0) + { + tetrisai_data->tetris.ai.findWorstMove = true; + tetrisai_data->tetris.poll(); + tetrisai_data->tetris.ai.findWorstMove = false; + tetrisai_data->mistaceCountdown = SEGMENT.custom3; + } + tetrisai_data->mistaceCountdown--; + } + tetrisai_data->tetris.poll(); + } + else + { + tetrisai_data->tetris.poll(); + } + + return FRAMETIME; +} // mode_2DTetrisAI() +static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11"; + +class TetrisAIUsermod : public Usermod +{ + +private: + +public: + void setup() + { + strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI); + } + + void loop() + { + + } + + uint16_t getId() + { + return USERMOD_ID_TETRISAI; + } +}; diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index 18d00da3c2..a7337eaf99 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -71,7 +71,7 @@ * if you want to receive two channels, one is the actual data from microphone and another channel is suppose to receive 0, it's different data in two channels, you need to choose I2S_CHANNEL_FMT_RIGHT_LEFT in this case. */ -#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 4)) +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0)) && (ESP_IDF_VERSION <= ESP_IDF_VERSION_VAL(4, 4, 6)) // espressif bug: only_left has no sound, left and right are swapped // https://github.com/espressif/esp-idf/issues/9635 I2S mic not working since 4.4 (IDFGH-8138) // https://github.com/espressif/esp-idf/issues/8538 I2S channel selection issue? (IDFGH-6918) @@ -770,4 +770,4 @@ class SPH0654 : public I2SSource { #endif } }; -#endif \ No newline at end of file +#endif diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 47804b611f..4668ca8814 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -27,18 +27,11 @@ Currently ESP8266 is not supported, due to low speed and small RAM of this chip. There are however plans to create a lightweight audioreactive for the 8266, with reduced features. ## Installation -### using customised _arduinoFFT_ library for use with this usermod -Add `-D USERMOD_AUDIOREACTIVE` to your PlatformIO environment `build_flags`, as well as `https://github.com/blazoncek/arduinoFFT.git` to your `lib_deps`. -If you are not using PlatformIO (which you should) try adding `#define USERMOD_AUDIOREACTIVE` to *my_config.h* and make sure you have _arduinoFFT_ library downloaded and installed. +### using latest _arduinoFFT_ library version 2.x +The latest arduinoFFT release version should be used for audioreactive. -Customised _arduinoFFT_ library for use with this usermod can be found at https://github.com/blazoncek/arduinoFFT.git - -### using latest (develop) _arduinoFFT_ library -Alternatively, you can use the latest arduinoFFT development version. -ArduinoFFT `develop` library is slightly more accurate, and slightly faster than our customised library, however also needs additional 2kB RAM. - -* `build_flags` = `-D USERMOD_AUDIOREACTIVE` `-D UM_AUDIOREACTIVE_USE_NEW_FFT` -* `lib_deps`= `https://github.com/kosme/arduinoFFT#develop @ 1.9.2` +* `build_flags` = `-D USERMOD_AUDIOREACTIVE` +* `lib_deps`= `kosme/arduinoFFT @ 2.0.1` ## Configuration diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index cb1eec8e1b..efb3c8ae19 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -667,7 +667,7 @@ void MultiRelay::addToJsonInfo(JsonObject &root) { for (int i=0; i Usermods + +### Usermod Setup + +* Global I2C GPIOs (HW) - Set the SDA and SCL pins + +### 4LineDisplay + +* `enabled` - enable/disable usermod +* `type` - display type in numeric format + * 1 = I2C SSD1306 128x32 + * 2 = I2C SH1106 128x32 + * 3 = I2C SSD1306 128x64 (4 double-height lines) + * 4 = I2C SSD1305 128x32 + * 5 = I2C SSD1305 128x64 (4 double-height lines) + * 6 = SPI SSD1306 128x32 + * 7 = SPI SSD1306 128x64 (4 double-height lines) + * 8 = SPI SSD1309 128x64 (4 double-height lines) + * 9 = I2C SSD1309 128x64 (4 double-height lines) +* `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST +* `flip` - flip/rotate display 180° +* `contrast` - set display contrast (higher contrast may reduce display lifetime) +* `screenTimeOutSec` - screen saver time-out in seconds +* `sleepMode` - enable/disable screen saver +* `clockMode` - enable/disable clock display in screen saver mode +* `showSeconds` - Show seconds on the clock display +* `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) + + ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 82a5e1a81d..2cb1507ce1 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -17,7 +17,7 @@ // for WLED. // // Dependencies -// * This Usermod works best, by far, when coupled +// * This Usermod works best, by far, when coupled // with RotaryEncoderUI ALT Usermod. // // Make sure to enable NTP and set your time zone in WLED Config | Time. @@ -89,7 +89,8 @@ typedef enum { SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI - SSD1309_SPI64 // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C } DisplayType; @@ -235,7 +236,7 @@ class FourLineDisplayUsermod : public Usermod { void updateSpeed(); void updateIntensity(); void drawStatusIcons(); - + /** * marks the position of the arrow showing * the current setting being changed @@ -246,8 +247,8 @@ class FourLineDisplayUsermod : public Usermod { //Draw the arrow for the current setting being changed void drawArrow(); - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); /** @@ -314,14 +315,14 @@ class FourLineDisplayUsermod : public Usermod { * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). - * + * * CAUTION: serializeConfig() will initiate a filesystem write operation. * It might cause the LEDs to stutter and will cause flash wear if called too often. * Use it sparingly and always in the loop, never in network callbacks! - * + * * addToConfig() will also not yet add your setting to one of the settings pages automatically. * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * + * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void addToConfig(JsonObject& root) override; @@ -329,7 +330,7 @@ class FourLineDisplayUsermod : public Usermod { /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * + * * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) @@ -494,7 +495,7 @@ void FourLineDisplayUsermod::showTime() { } if (knownHour != hourCurrent) { // only update date when hour changes - sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); + sprintf_P(lineBuffer, PSTR("%s %2d "), monthShortStr(month(localTime)), day(localTime)); draw2x2String(2, lineHeight==1 ? 0 : lineHeight, lineBuffer); // adjust for 8 line displays, draw month and day } sprintf_P(lineBuffer,PSTR("%2d:%02d"), (useAMPM ? AmPmHour : hourCurrent), minuteCurrent); @@ -556,6 +557,7 @@ void FourLineDisplayUsermod::setup() { case SSD1306_64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_HW_I2C(); break; case SSD1305: u8x8 = (U8X8 *) new U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C(); break; case SSD1305_64: u8x8 = (U8X8 *) new U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C(); break; + case SSD1309_64: u8x8 = (U8X8 *) new U8X8_SSD1309_128X64_NONAME0_HW_I2C(); break; // U8X8 uses global SPI variable that is attached to VSPI bus on ESP32 case SSD1306_SPI: u8x8 = (U8X8 *) new U8X8_SSD1306_128X32_UNIVISION_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset case SSD1306_SPI64: u8x8 = (U8X8 *) new U8X8_SSD1306_128X64_NONAME_4W_HW_SPI(ioPin[0], ioPin[1], ioPin[2]); break; // Pins are cs, dc, reset @@ -581,7 +583,7 @@ void FourLineDisplayUsermod::setup() { // gets called every time WiFi is (re-)connected. Initialize own network // interfaces here void FourLineDisplayUsermod::connected() { - knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : + knownSsid = WiFi.SSID(); //apActive ? apSSID : WiFi.SSID(); //apActive ? WiFi.softAPSSID() : knownIp = Network.localIP(); //apActive ? IPAddress(4, 3, 2, 1) : Network.localIP(); networkOverlay(PSTR("NETWORK INFO"),7000); } @@ -637,7 +639,7 @@ void FourLineDisplayUsermod::redraw(bool forceRedraw) { powerON = !powerON; drawStatusIcons(); return; - } else if (knownnightlight != nightlightActive) { //trigger moon icon + } else if (knownnightlight != nightlightActive) { //trigger moon icon knownnightlight = nightlightActive; drawStatusIcons(); if (knownnightlight) { @@ -652,7 +654,7 @@ void FourLineDisplayUsermod::redraw(bool forceRedraw) { return; } else if (knownMode != effectCurrent || knownPalette != effectPalette) { if (displayTurnedOff) needRedraw = true; - else { + else { if (knownPalette != effectPalette) { showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); knownPalette = effectPalette; } if (knownMode != effectCurrent) { showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); knownMode = effectCurrent; } lastRedraw = now; @@ -703,7 +705,7 @@ void FourLineDisplayUsermod::redraw(bool forceRedraw) { drawArrow(); drawStatusIcons(); - // Second row + // Second row updateBrightness(); updateSpeed(); updateIntensity(); @@ -805,8 +807,8 @@ void FourLineDisplayUsermod::drawArrow() { lockRedraw = false; } -//Display the current effect or palette (desiredEntry) -// on the appropriate line (row). +//Display the current effect or palette (desiredEntry) +// on the appropriate line (row). void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) unsigned long now = millis(); @@ -857,7 +859,7 @@ void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const c while (smallChars1 < (MAX_MODE_LINE_SPACE-1)) smallBuffer1[smallChars1++]=' '; smallBuffer1[smallChars1] = 0; drawString(1, row*lineHeight, smallBuffer1, true); - while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; + while (smallChars2 < (MAX_MODE_LINE_SPACE-1)) smallBuffer2[smallChars2++]=' '; smallBuffer2[smallChars2] = 0; drawString(1, row*lineHeight+1, smallBuffer2, true); } @@ -1133,10 +1135,12 @@ bool FourLineDisplayUsermod::handleButton(uint8_t b) { return handled; } -#if CONFIG_FREERTOS_UNICORE -#define ARDUINO_RUNNING_CORE 0 -#else -#define ARDUINO_RUNNING_CORE 1 +#ifndef ARDUINO_RUNNING_CORE + #if CONFIG_FREERTOS_UNICORE + #define ARDUINO_RUNNING_CORE 0 + #else + #define ARDUINO_RUNNING_CORE 1 + #endif #endif void FourLineDisplayUsermod::onUpdateBegin(bool init) { #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) @@ -1150,7 +1154,7 @@ void FourLineDisplayUsermod::onUpdateBegin(bool init) { xTaskCreatePinnedToCore( [](void * par) { // Function to implement the task // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; + const TickType_t xFrequency = REFRESH_RATE_MS * portTICK_PERIOD_MS / 2; TickType_t xLastWakeTime = xTaskGetTickCount(); for(;;) { delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. @@ -1205,6 +1209,7 @@ void FourLineDisplayUsermod::appendConfigData() { oappend(SET_F("addOption(dd,'SSD1306 128x64',3);")); oappend(SET_F("addOption(dd,'SSD1305',4);")); oappend(SET_F("addOption(dd,'SSD1305 128x64',5);")); + oappend(SET_F("addOption(dd,'SSD1309 128x64',9);")); oappend(SET_F("addOption(dd,'SSD1306 SPI',6);")); oappend(SET_F("addOption(dd,'SSD1306 SPI 128x64',7);")); oappend(SET_F("addOption(dd,'SSD1309 SPI 128x64',8);")); @@ -1218,14 +1223,14 @@ void FourLineDisplayUsermod::appendConfigData() { * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * If you want to force saving the current state, use serializeConfig() in your loop(). - * + * * CAUTION: serializeConfig() will initiate a filesystem write operation. * It might cause the LEDs to stutter and will cause flash wear if called too often. * Use it sparingly and always in the loop, never in network callbacks! - * + * * addToConfig() will also not yet add your setting to one of the settings pages automatically. * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * + * * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { @@ -1252,7 +1257,7 @@ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { /* * readFromConfig() can be used to read back the custom settings you added with addToConfig(). * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * + * * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) @@ -1346,6 +1351,10 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1305_128x64_adafruit, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); break; + case SSD1309_64: + u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1309_128x64_noname0, u8x8_cad_ssd13xx_fast_i2c, u8x8_byte_arduino_hw_i2c, u8x8_gpio_and_delay_arduino); + u8x8_SetPin_HW_I2C(u8x8->getU8x8(), U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE); + break; case SSD1306_SPI: u8x8_Setup(u8x8->getU8x8(), u8x8_d_ssd1306_128x32_univision, u8x8_cad_001, u8x8_byte_arduino_hw_spi, u8x8_gpio_and_delay_arduino); u8x8_SetPin_4Wire_HW_SPI(u8x8->getU8x8(), ioPin[0], ioPin[1], ioPin[2]); // Pins are cs, dc, reset diff --git "a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio\342\200\223override.sample.ini" "b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio\342\200\223override.sample.ini" new file mode 100644 index 0000000000..6b32c71fbf --- /dev/null +++ "b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio\342\200\223override.sample.ini" @@ -0,0 +1,17 @@ +[platformio] +default_envs = esp32dev + +[env:esp32dev] +board = esp32dev +platform = ${esp32.platform} +build_unflags = ${common.build_unflags} +build_flags = + ${common.build_flags_esp32} + -D USERMOD_FOUR_LINE_DISPLAY -D USE_ALT_DISPlAY + -D USERMOD_ROTARY_ENCODER_UI -D ENCODER_DT_PIN=18 -D ENCODER_CLK_PIN=5 -D ENCODER_SW_PIN=19 +upload_speed = 460800 +lib_deps = + ${esp32.lib_deps} + U8g2@~2.34.4 + Wire + diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index 516362380d..10db879fb3 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -8,18 +8,18 @@ The core of these usermods are a copy of the originals. The main changes are to The display usermod UI has been completely changed. -The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. +The changes made to the RotaryEncoder usermod were made to support the new UI in the display usermod. Without the display, it functions identical to the original. The original "usermod_v2_auto_save" will not work with the display just yet. Press the encoder to cycle through the options: - *Brightness - *Speed - *Intensity - *Palette - *Effect - *Main Color (only if display is used) - *Saturation (only if display is used) +* Brightness +* Speed +* Intensity +* Palette +* Effect +* Main Color (only if display is used) +* Saturation (only if display is used) Press and hold the encoder to display Network Info if AP is active, it will display the AP, SSID and Password @@ -30,10 +30,23 @@ Also shows if the timer is enabled. ## Installation -Please refer to the original `usermod_v2_rotary_encoder_ui` readme for the main instructions.
-To activate this alternative usermod, add `#define USE_ALT_DISPlAY` to the `usermods_list.cpp` file, -or add `-D USE_ALT_DISPlAY` to the original `platformio_override.ini.sample` file. +Copy the example `platformio_override.sample.ini` to the root directory of your particular build and rename it to `platformio_override.ini`. +To activate this alternative usermod, add `#define USE_ALT_DISPlAY` (NOTE: CASE SENSITIVE) to the `usermods_list.cpp` file, or add `-D USE_ALT_DISPlAY` to your `platformio_override.ini` file + +### Define Your Options + +* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp +* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp + also tells this usermod that the display is available + (see the Four Line Display usermod `readme.md` for more details) +* `USE_ALT_DISPlAY` - Mandatory to use Four Line Display +* `ENCODER_DT_PIN` - defaults to 18 +* `ENCODER_CLK_PIN` - defaults to 5 +* `ENCODER_SW_PIN` - defaults to 19 +* `USERMOD_ROTARY_ENCODER_GPIO` - GPIO functionality: + `INPUT_PULLUP` to use internal pull-up + `INPUT` to use pull-up on the PCB ### PlatformIO requirements diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 6a15b520bd..e5a5f24f78 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -392,26 +392,26 @@ byte RotaryEncoderUIUsermod::readPin(uint8_t pin) { * modes_alpha_indexes and palettes_alpha_indexes. */ void RotaryEncoderUIUsermod::sortModesAndPalettes() { - DEBUG_PRINTLN(F("Sorting modes and palettes.")); + DEBUG_PRINT(F("Sorting modes: ")); DEBUG_PRINTLN(strip.getModeCount()); //modes_qstrings = re_findModeStrings(JSON_mode_names, strip.getModeCount()); modes_qstrings = strip.getModeDataSrc(); modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), MODE_SORT_SKIP_COUNT); - palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()+strip.customPalettes.size()); - palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()+strip.customPalettes.size()); + DEBUG_PRINT(F("Sorting palettes: ")); DEBUG_PRINT(strip.getPaletteCount()); DEBUG_PRINT('/'); DEBUG_PRINTLN(strip.customPalettes.size()); + palettes_qstrings = re_findModeStrings(JSON_palette_names, strip.getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(strip.getPaletteCount()); if (strip.customPalettes.size()) { for (int i=0; i> 2) +8); - uint16_t index = (counter * SEGLEN) >> 16; - SEGMENT.fade_out(SEGMENT.intensity); + const unsigned speed = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range + const unsigned pixels = SEGLEN / speed; // how many pixels to advance per frame - if (SEGENV.step > index && SEGENV.step - index > SEGLEN/2) { - SEGENV.aux0 = !SEGENV.aux0; - } + SEGMENT.fade_out(255-SEGMENT.intensity); + + if (SEGENV.step > strip.now) return FRAMETIME; // we have a pause - for (int i = SEGENV.step; i < index; i++) { - uint16_t j = (SEGENV.aux0)?i:SEGLEN-1-i; - SEGMENT.setPixelColor( j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); + unsigned index = SEGENV.aux1 + pixels; + // are we slow enough to use frames per pixel? + if (pixels == 0) { + const unsigned frames = speed / SEGLEN; // how many frames per 1 pixel + if (SEGENV.step++ < frames) return FRAMETIME; + SEGENV.step = 0; + index++; } - if (dual) { - uint32_t c; - if (SEGCOLOR(2) != 0) { - c = SEGCOLOR(2); - } else { - c = SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); - } - for (int i = SEGENV.step; i < index; i++) { - uint16_t j = (SEGENV.aux0)?SEGLEN-1-i:i; + if (index > SEGLEN) { + + SEGENV.aux0 = !SEGENV.aux0; // change direction + SEGENV.aux1 = 0; // reset position + // set delay + if (SEGENV.aux0 || SEGMENT.check2) SEGENV.step = strip.now + SEGMENT.custom1 * 25; // multiply by 25ms + else SEGENV.step = 0; + + } else { + + // paint as many pixels as needed + for (unsigned i = SEGENV.aux1; i < index; i++) { + unsigned j = (SEGENV.aux0) ? i : SEGLEN - 1 - i; + uint32_t c = SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0); SEGMENT.setPixelColor(j, c); + if (SEGMENT.check1) { + SEGMENT.setPixelColor(SEGLEN - 1 - j, SEGCOLOR(2) ? SEGCOLOR(2) : c); + } } + SEGENV.aux1 = index; } - - SEGENV.step = index; return FRAMETIME; } - - -/* - * K.I.T.T. - */ -uint16_t mode_larson_scanner(void){ - return larson_scanner(false); -} -static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Fade rate;!,!;!;;m12=0"; - +static const char _data_FX_MODE_LARSON_SCANNER[] PROGMEM = "Scanner@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* * Creates two Larson scanners moving in opposite directions * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/DualLarson.h */ uint16_t mode_dual_larson_scanner(void){ - return larson_scanner(true); + SEGMENT.check1 = true; + return mode_larson_scanner(); } -static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Fade rate;!,!,!;!;;m12=0"; +static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; /* @@ -3016,8 +3021,12 @@ uint16_t mode_bouncing_balls(void) { } int pos = roundf(balls[i].height * (SEGLEN - 1)); + #ifdef WLED_USE_AA_PIXELS if (SEGLEN<32) SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index else SEGMENT.setPixelColor(balls[i].height + (stripNr+1)*10.0f, color); + #else + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); // encode virtual strip into index + #endif } } }; @@ -5903,6 +5912,7 @@ static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) +/* #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -5987,12 +5997,13 @@ uint16_t mode_2Dghostrider(void) { return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; - +*/ //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) +/* #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up @@ -6050,8 +6061,8 @@ uint16_t mode_2Dfloatingblobs(void) { } } uint32_t c = SEGMENT.color_from_palette(blob->color[i], false, false, 0); - if (blob->r[i] > 1.f) SEGMENT.fill_circle(blob->x[i], blob->y[i], roundf(blob->r[i]), c); - else SEGMENT.setPixelColorXY(blob->x[i], blob->y[i], c); + if (blob->r[i] > 1.f) SEGMENT.fill_circle(roundf(blob->x[i]), roundf(blob->y[i]), roundf(blob->r[i]), c); + else SEGMENT.setPixelColorXY((int)roundf(blob->x[i]), (int)roundf(blob->y[i]), c); // move x if (blob->x[i] + blob->r[i] >= cols - 1) blob->x[i] += (blob->sX[i] * ((cols - 1 - blob->x[i]) / blob->r[i] + 0.005f)); else if (blob->x[i] - blob->r[i] <= 0) blob->x[i] += (blob->sX[i] * (blob->x[i] / blob->r[i] + 0.005f)); @@ -6087,7 +6098,7 @@ uint16_t mode_2Dfloatingblobs(void) { } #undef MAX_BLOBS static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; - +*/ //////////////////////////// // 2D Scrolling text // @@ -7887,61 +7898,57 @@ static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitu * Uses palette for particle color * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 8 uint16_t mode_particlevortex(void) { if (SEGLEN == 1) return mode_static(); - uint8_t numSprays; // maximum number of sprays ParticleSystem *PartSys = NULL; uint32_t i = 0; uint32_t j = 0; - uint8_t spraycount = 1 + (SEGMENT.custom1 >> 5); // number of sprays to display, 1-8 - - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed; //allocation failed //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - // TODO: use SEGMENT.step for smooth direction change - numSprays = min(PartSys->numSources, (uint8_t) 8); + #ifdef ESP8266 + PartSys->setMotionBlur(150); + #else + PartSys->setMotionBlur(100); + #endif + uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.sat = 255; // set saturation - PartSys->sources[i].source.x = (PartSys->maxX - PS_P_HALFRADIUS + 1) >> 1; // center - PartSys->sources[i].source.y = (PartSys->maxY - PS_P_HALFRADIUS + 1) >> 1; // center - PartSys->sources[i].source.vx = 0; - PartSys->sources[i].source.vy = 0; + { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; - PartSys->sources[i].minLife = 800; //!!! - PartSys->sources[i].vx = 0; // emitting speed - PartSys->sources[i].vy = 0; // emitting speed - PartSys->sources[i].var = 0; // emitting variation - if (SEGMENT.check1) // random color is checked - PartSys->sources[i].source.hue = random16(); - else - { - uint8_t coloroffset = 0xFF / spraycount; - PartSys->sources[i].source.hue = coloroffset * i; - } + PartSys->sources[i].minLife = 800; } PartSys->setKillOutOfBounds(true); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - //DEBUG_PRINTF_P(PSTR("segment data ptr in candy FX %p\n"), SEGMENT.data); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numSprays = min(PartSys->numSources, (uint8_t)8); - - if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01)) //state change + uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + #ifdef ESP8266 + for (i = 1; i < 4; i++) //need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + { + PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[PartSys->numParticles - i].sat = 230; + PartSys->particles[PartSys->numParticles - i].ttl = 100; //set alive + } + #endif + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change { if (SEGMENT.check1) SEGMENT.aux1 |= 0x01; //set the flag @@ -7962,12 +7969,15 @@ uint16_t mode_particlevortex(void) } } - // set rotation direction and speed TODO: use SEGMENT.step to increment until speed is reached, increment speed depends on rotation speed as well fast rotations speed up faster too + //TODO: speed increment is still wrong, it only works in autochange, manual change to a lower speed does not work. need to make it better. + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - + int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step + if (SEGMENT.custom2 > 0) // automatic direction change enabled { + // speedincrement = 1 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag @@ -7986,47 +7996,61 @@ uint16_t mode_particlevortex(void) } } - int32_t currentspeed = (int32_t)SEGMENT.step; //make a signed integer out of step - int32_t speedincrement = 20 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); - if (direction) - { - if(currentspeed < (SEGMENT.speed << 2)) //speed is not on target speed yet - currentspeed += speedincrement; - } - else + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 2); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; + + if (speedincrement == 0) //if speeddiff is not zero, make the increment at least 1 so it reaches target speed { - if (currentspeed > -(SEGMENT.speed << 2)) // speed is not on target speed yet - currentspeed -= speedincrement; + if(speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; } + + currentspeed += speedincrement; SEGMENT.aux0 += currentspeed; SEGMENT.step = (uint32_t)currentspeed; //save it back // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; - for (j = 0; j < spraycount; j++) - { + //int32_t particlespeeddiv = ((263 - SEGMENT.intensity) >> 3); + //int32_t particlespeed = 127/particlespeeddiv; //just for testing, need to replace this with angle emit and come up with a new speed calculation + //particle speed goes from 7 to 128 (sin cos return 15bit value but with sign) + + // for (j = 0; j < spraycount; j++) //TODO: use angle emit + // { + // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / ((263 - SEGMENT.intensity) >> 3); // update spray angle (rotate all sprays with angle offset) - PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) - } + // PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) + // PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) + // PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) + // } -//TODO: limit the emit amount by particle speed. should not emit more than one for every speed of like 20 or so, it looks weird on initialisation also make it depnd on angle speed, emit no more than once every few degrees -> less overlap (need good finetuning) - j = random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. - for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) +//test to check if less particles are ok at lower speeds. + uint32_t skip = PS_P_HALFRADIUS/(SEGMENT.intensity + 1) + 1; + if (SEGMENT.call % skip == 0) { - #ifdef ESP8266 - if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles - #endif - PartSys->sprayEmit(PartSys->sources[j]); - j = (j + 1) % spraycount; + j = random(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < spraycount; i++) // emit one particle per spray (if available) + { + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGMENT.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + //PartSys->sprayEmit(PartSys->sources[j]); + j = (j + 1) % spraycount; + } } - PartSys->update(); //update all particles and render to frame + + SEGMENT.blur(50); //TODO: put this in particle system for faster rendering return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Auto Flip,Nozzle,Random Color, Direction, Random Flip;;!;2;pal=56,sx=200,ix=190,c1=200,c2=0,c3=0,o1=0,o2=0,o3=0"; /* @@ -8035,7 +8059,7 @@ static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation S * Uses ranbow palette as default * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 4 uint16_t mode_particlefireworks(void) { if (SEGLEN == 1) @@ -8047,11 +8071,11 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); //ground bounce is fixed - numRockets = min(PartSys->numSources, (uint8_t)4); + PartSys->setWallHardness(100); //ground bounce is fixed + numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon @@ -8059,7 +8083,7 @@ uint16_t mode_particlefireworks(void) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8067,12 +8091,12 @@ uint16_t mode_particlefireworks(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = min(PartSys->numSources, (uint8_t)4); + numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed - PartSys->enableGravity(true, map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8099,11 +8123,11 @@ uint16_t mode_particlefireworks(void) } else // speed is zero, explode! { - #ifdef ESP8266 + #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion - #else + #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion - #endif + #endif PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch if(random16(4) == 0) { @@ -8176,13 +8200,13 @@ uint16_t mode_particlefireworks(void) else if ( PartSys->sources[j].source.vy < 0) // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it { // reinitialize rocket - PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random16(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) - PartSys->sources[j].maxLife = 30; // exhaust particle life + PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = 0; // emitting speed @@ -8193,7 +8217,8 @@ uint16_t mode_particlefireworks(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,,Gravity,Cylinder,Ground,;;!;2;pal=11,sx=100,ix=50,c1=84,c2=128,c3=12,o1=0,o2=0,o3=0"; /* * Particle Volcano @@ -8201,6 +8226,7 @@ static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Laun * Uses palette for particle color * by DedeHai (Damian Schneider) */ +#define NUMBEROFSOURCES 1 uint16_t mode_particlevolcano(void) { if (SEGLEN == 1) @@ -8211,25 +8237,24 @@ uint16_t mode_particlevolcano(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed - PartSys->setBounceY(true); - PartSys->enableGravity(true); //enable with default gforce - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - numSprays = min(PartSys->numSources, (uint8_t)1); //number of sprays + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); //anable motion blur + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation + PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].vx = 0; // emitting speed + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8237,7 +8262,7 @@ uint16_t mode_particlevolcano(void) return mode_static(); // something went wrong, no data! } - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8274,13 +8299,14 @@ uint16_t mode_particlevolcano(void) PartSys->update(); // update and render return FRAMETIME; } +#undef NUMBEROFSOURCES static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,Color by Age,Walls,Collisions;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1,o2=0,o3=0"; - /* - * Particle Fire - * realistic fire effect using particles. heat based and using perlin-noise for wind - * by DedeHai (Damian Schneider) - */ +/* +* Particle Fire +* realistic fire effect using particles. heat based and using perlin-noise for wind +* by DedeHai (Damian Schneider) +*/ uint16_t mode_particlefire(void) { if (SEGLEN == 1) @@ -8292,24 +8318,15 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) + if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - Serial.println("fireinit done"); SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise // initialize the flame sprays numFlames = PartSys->numSources; - for (i = 0; i < numFlames; i++) - { - PartSys->sources[i].source.ttl = 0; - PartSys->sources[i].source.vx = 0; // emitter moving speed; - PartSys->sources[i].source.vy = 0; - // note: other parameters are set when creating the flame (see blow) - } - DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8317,14 +8334,29 @@ uint16_t mode_particlefire(void) return mode_static(); // something went wrong, no data! } - // DEBUG_PRINTF_P(PSTR("segment data ptr in Fire FX %p\n"), SEGMENT.data); PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 120); // anable/disable motion blur + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) //slow, limit FPS + { + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = strip.now - *lastcall; + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS + { + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating it + return FRAMETIME; //do not update this frame + } + *lastcall = strip.now; + } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) - // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: will this give better flames or worse? - PartSys->setWrapX(SEGMENT.check2); + // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? // update the flame sprays: for (i = 0; i < numFlames; i++) @@ -8340,17 +8372,13 @@ uint16_t mode_particlefire(void) { PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width } - PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame - //PartSys->sources[i].source.ttl = 10 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (SEGMENT.speed >> 5)); //old not really good, too intense - PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (SEGMENT.speed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! - //PartSys->sources[i].source.ttl = 5 + random16(SEGMENT.custom1) / (1 + (SEGMENT.speed >> 5)); // this is experimental, fine tuning all parameters + PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random16(4) - 2; // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (SEGMENT.speed >> 2); // emitting speed (upwards) -> this is good - //PartSys->sources[i].var = (random16(5) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers - PartSys->sources[i].var = (random16(2 + (SEGMENT.speed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].vx = (int8_t)random(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good + PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers } } @@ -8358,79 +8386,42 @@ uint16_t mode_particlefire(void) if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call & 0x02) //every tird frame + if (SEGMENT.call & 0x02) //every third frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often - // add wind force to all particles - int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 12; - PartSys->applyForce(PartSys->particles, PartSys->usedParticles, windspeed, 0); + int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 7; + PartSys->applyForce(windspeed, 0); } SEGMENT.step++; -//this is a work in progress... hard to find settings that look good TODO: looks ok on speed 130, need to tune it for other speeds - if (SEGMENT.check3) + if (SEGMENT.check3) //add turbulance (parameters and algorithm found by experimentation) { - if (SEGMENT.call % map(SEGMENT.speed,0,255,4,15)==0) // update noise position every xth frames, also add wind -> has do be according to speed. 135-> every third frame - { - for (i = 0; i < PartSys->usedParticles; i++) + if (SEGMENT.call % map(firespeed,0,255,4,15)==0) { - //if (PartSys->particles[i].y > (PS_P_RADIUS << 1) && PartSys->particles[i].y < PartSys->maxY - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere - if (PartSys->particles[i].y < PartSys->maxY/4)// - (PS_P_RADIUS * 4)) // do not apply turbulance everywhere -> bottom quarter seems a good balance + for (i = 0; i < PartSys->usedParticles; i++) { - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x , PartSys->particles[i].y >> 1, SEGMENT.step<<2 ) - 127); - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); - int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); //-> this is good! - - //int32_t curl = ((int16_t)inoise8(PartSys->particles[i].x>>1, SEGMENT.step<<5) - 127); - // curl = ((curl * PartSys->particles[i].y) / PartSys->maxY); //'curl' stronger at the top - //int modulation = inoise8(SEGMENT.step<<3, SEGMENT.aux1) ; - //PartSys->particles[i].vx += curl>>2; - //PartSys->particles[i].vy += curl>>3; - //PartSys->particles[i].vx += (curl * ((SEGMENT.custom2 * modulation)>>7)) >> 9; - //PartSys->particles[i].vy += ((curl ) * ((SEGMENT.custom2 * modulation)>>7))>>10; - //PartSys->particles[i].vx += (curl * curl * (SEGMENT.speed+10)) >> 14; //this may be a bad idea -> yes, squre is always positive... and a bit strong - PartSys->particles[i].vx += (curl * (SEGMENT.speed + 10)) >> 9; //-> this is not too bad! - // PartSys->particles[i].vy += (curl * SEGMENT.custom2 ) >> 13; + if (PartSys->particles[i].y < PartSys->maxY/4) // do not apply turbulance everywhere -> bottom quarter seems a good balance + { + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y , SEGMENT.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + + } } } } - } - uint8_t j = random16(numFlames); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + uint8_t j = random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) for(i=0; i < percycle; i++) { - PartSys->flameEmit(PartSys->sources[j]); j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); } -/* - j=5; - //a test: emitting one base particle per frame - if (SEGMENT.check1) - { - for (i = 0; i < PartSys->usedParticles; i++) // emit particles //todo: if this works, make it the last spray - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].vy = 1 ;//+ (SEGMENT.speed >> 3); - PartSys->particles[i].ttl = 10;//(PS_P_RADIUS<<2) / PartSys->particles[i].vy; - PartSys->particles[i].x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; - PartSys->particles[i].y = 0; - PartSys->particles[i].vx = 0;//(((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1) + 5) >> 1; // side speed is +/- a quarter of the custom1 slider - Serial.print("*"); - j--; - } - if(j==0) break; - } - Serial.println("B"); - }*/ - PartSys->updateFire(SEGMENT.intensity); // update and render the fire return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,,Cylinder,Turbulence;;!;2;pal=35,sx=130,ix=120,c1=110,c2=128,c3=22,o1=0"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8447,14 +8438,14 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) //init + if (!initParticleSystem(PartSys, 1, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); - PartSys->enableGravity(true); + PartSys->setGravity(); //enable with default gravity PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8465,7 +8456,7 @@ uint16_t mode_particlepit(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); PartSys->setBounceY(SEGMENT.check3); - PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); //limit to 100 min (if collisions are disabled, still want bouncy) if (SEGMENT.custom2>0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8474,40 +8465,175 @@ uint16_t mode_particlepit(void) PartSys->enableParticleCollisions(false); } - uint32_t i; // index variable - - if (SEGMENT.call % (64 - (SEGMENT.intensity >> 2)) == 0 && SEGMENT.intensity > 1) // every nth frame emit particles, stop emitting if set to zero + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero + { + for (i = 0; i < PartSys->usedParticles; i++) // emit particles { - for (i = 0; i < PartSys->usedParticles; i++) // emit particles + if (PartSys->particles[i].ttl == 0) // find a dead particle { - if (PartSys->particles[i].ttl == 0) // find a dead particle + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; //enable collision for particle + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; + //set particle size + if(SEGMENT.custom1 == 255) + { + PartSys->setParticleSize(0); //set global size to zero + PartSys->advPartProps[i].size = random(SEGMENT.custom1); //set each particle to random size + } + else { - // emit particle at random position over the top of the matrix (random16 is not random enough) - PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); - PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height - PartSys->particles[i].vx = (((int16_t)random(SEGMENT.custom1)) - (SEGMENT.custom1 >> 1)+5) >> 1; // side speed is +/- a quarter of the custom1 slider - PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set saturation - PartSys->particles[i].collide = true; //enable collision for particle - break; // emit only one particle per round + PartSys->setParticleSize(SEGMENT.custom1); //set global size + PartSys->advPartProps[i].size = 0; //use global size } + break; // emit only one particle per round } } - - uint32_t frictioncoefficient = 1; + } + + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; - if (SEGMENT.call % (3+(SEGMENT.custom2>>2)) == 0) + //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) + //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) //note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); - PartSys->update(); // update and render + PartSys->update(); // update and render + +//Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: + /* + SEGMENT.blur(SEGMENT.custom1, true); + if (SEGMENT.custom1 > 64) + SEGMENT.blur(SEGMENT.custom1 - 64, true); + if (SEGMENT.custom1 > 128) + SEGMENT.blur((SEGMENT.custom1 - 128) << 1, true); + if (SEGMENT.custom1 > 192) + SEGMENT.blur((SEGMENT.custom1 - 192) << 1, true); + */ +/* +//wobbling + static uint8_t testcntr; + static uint8_t wobbleamount = 200; + wobbleamount -= 2; + + testcntr+=15; + +// int32_t ysize = (int16_t)sin8(testcntr); + // int32_t xsize = 255-ysize; + + int32_t ysize = (int32_t)sin8(testcntr)-128; + int32_t xsize = -ysize; //TODO: xsize is not really needed, calculation can be simplified using just ysize + + //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); + //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); + ysize = (int32_t)SEGMENT.custom1 - ((ysize * wobbleamount * SEGMENT.custom1) >> 15); + xsize = (int32_t)SEGMENT.custom1 - ((xsize * wobbleamount * SEGMENT.custom1) >> 15); + + Serial.print(SEGMENT.custom1); + Serial.print(" "); + Serial.print(wobbleamount); + Serial.print(" "); + + Serial.print(xsize); + Serial.print(" "); + Serial.print(ysize); + + + const unsigned cols = PartSys->maxXpixel + 1; + const unsigned rows = PartSys->maxYpixel + 1; + uint8_t xiterations = 1 + (xsize>>8); //allow for wobble size > 255 + uint8_t yiterations = 1 + (ysize>>8); + uint8_t secondpassxsize = xsize - 255; + uint8_t secondpassysize = ysize - 255; + if (xsize > 255) + xsize = 255; //first pass, full sized + if (ysize > 255) + ysize = 255; + + Serial.print(xsize); + Serial.print(" "); + Serial.println(ysize); + for (uint32_t j = 0; j < xiterations; j++) + { + for (uint32_t i = 0; i < cols; i++) + { + SEGMENT.blurCol(i, xsize, true); + if (xsize > 64) + SEGMENT.blurCol(i, xsize - 64, true); + if (xsize > 128) + SEGMENT.blurCol(i, (xsize - 128) << 1, true); + if (xsize > 192) + SEGMENT.blurCol(i, (xsize - 192) << 1, true); + } + //set size for second pass: + xsize = secondpassxsize; + } + for (uint32_t j = 0; j < yiterations; j++) + { + for (unsigned i = 0; i < rows; i++) + { + SEGMENT.blurRow(i, ysize, true); + if (ysize > 64) + SEGMENT.blurRow(i, ysize - 64, true); + if (ysize > 128) + SEGMENT.blurRow(i, (ysize - 128) << 1, true); + if (ysize > 192) + SEGMENT.blurRow(i, (ysize - 192) << 1, true); + } + // set size for second pass: + ysize = secondpassysize; + } +*/ - return FRAMETIME; - } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Randomness,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=100,c3=31,o1=0,o2=0,o3=1"; + +/* +//rotat image (just a test, non working yet) + float angle = PI/3; + // Calculate sine and cosine of the angle + float cosTheta = cos(angle); + float sinTheta = sin(angle); + + // Center of rotation + int centerX = cols / 2; + int centerY = rows / 2; + + // Iterate over each pixel in the output image + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < cols; x++) + { + int relX = x - centerX; + int relY = y - centerY; + + // Apply rotation using axis symmetry + int origX = round(relX * cosTheta - relY * sinTheta) + centerX; + int origY = round(relX * sinTheta + relY * cosTheta) + centerY; + + // Check if original coordinates are within bounds + if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) + { + // Copy pixel value from original image to rotated image + SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); + } + + // Copy pixel values from original image to rotated image + rotatedImage[origY][origX] = image[y][x]; + rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; + rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; + rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; + } + }*/ + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8524,16 +8650,15 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed - return mode_static(); // allocation failed; //allocation failed - PartSys->enableGravity(true); // enable with default gforce - PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); //anable motion blur numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation - PartSys->sources[i].source.vx = 0; PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) @@ -8542,16 +8667,15 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; #endif - PartSys->sources[i].vx = 0; // emitting speed PartSys->sources[i].var = 7; // emiting variation } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } // Particle System settings @@ -8615,15 +8739,15 @@ uint16_t mode_particlebox(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed; //allocation failed } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } @@ -8631,11 +8755,7 @@ uint16_t mode_particlebox(void) PartSys->setBounceX(true); PartSys->setBounceY(true); PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more - if (SEGMENT.custom2 > 0) - PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness - else - PartSys->enableParticleCollisions(false); - + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness #ifdef ESP8266 uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); @@ -8650,8 +8770,7 @@ uint16_t mode_particlebox(void) for (i = 0; i < maxnumParticles; i++) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].hue = i * 5; // color range - PartSys->particles[i].sat = 255; // set full saturation (lets palette choose the color) + PartSys->particles[i].hue = i * 5; // color range PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction PartSys->particles[i].collide = true; // all particles collide @@ -8670,17 +8789,20 @@ uint16_t mode_particlebox(void) if(SEGMENT.check1) //random, use perlin noise { - xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); // TODO: inoise 16 would be faster? + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - // scale the gravity force down - xgravity /= 2 + ((256 - SEGMENT.custom1) >> 3); //(divide by 1-32) - ygravity /= 2 + ((256 - SEGMENT.custom1) >> 3); + // scale the gravity force + //xgravity /= 1 + ((256 - SEGMENT.custom1) >> 5); + //ygravity /= 1 + ((256 - SEGMENT.custom1) >> 5); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 50; + ygravity = (ygravity * SEGMENT.custom1) / 50; } else //go in a circle { // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle - xgravity = ((int32_t)(SEGMENT.custom1 >> 3) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; - ygravity = ((int32_t)(SEGMENT.custom1 >> 3) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; + xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } if (SEGMENT.check3) //sloshing, y force is alwys downwards { @@ -8688,7 +8810,7 @@ uint16_t mode_particlebox(void) ygravity = -ygravity; } - PartSys->applyForce(PartSys->particles, PartSys->usedParticles, xgravity, ygravity); + PartSys->applyForce(xgravity, ygravity); // reset particle TTL so they never die for (i = 0; i < PartSys->usedParticles; i++) @@ -8722,18 +8844,17 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { - PartSys->particles[i].sat = 255; // full saturation, color set by palette PartSys->particles[i].collide = true; // all particles colllide } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -8748,29 +8869,28 @@ uint16_t mode_particleperlin(void) PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); - + PartSys->setMotionBlur(230); // anable motion blur // apply 'gravity' from a 2D perlin noise map SEGMENT.aux0 += 1+(SEGMENT.speed >> 5); // noise z-position - // update position in noise for (i = 0; i < displayparticles; i++) { if (PartSys->particles[i].ttl == 0) // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) { PartSys->particles[i].ttl = random16(500) + 200; - PartSys->particles[i].x = random16(PartSys->maxX); - PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random(PartSys->maxY); } uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; int16_t baseheight = inoise8(xnoise, ynoise, SEGMENT.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value - if (SEGMENT.call % 10 == 0) // do not apply the force every frame, is too chaotic + if (SEGMENT.call % 8 == 0) // do not apply the force every frame, is too chaotic { - int8_t xslope = (baseheight - (int16_t)inoise8(xnoise + 10, ynoise, SEGMENT.aux0)); - int8_t yslope = (baseheight - (int16_t)inoise8(xnoise, ynoise + 10, SEGMENT.aux0)); - PartSys->applyForce(&(PartSys->particles[i]), 1, xslope, yslope); + int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGMENT.aux0)); + int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGMENT.aux0)); + PartSys->applyForce(i, xslope, yslope); } } @@ -8780,13 +8900,13 @@ uint16_t mode_particleperlin(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=54,sx=75,ix=200,c1=255,c2=180,c3=20,o1=0"; +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,,Collisions;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o1=0,o3=1"; /* * Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with * by DedeHai (Damian Schneider) */ - +#define NUMBEROFSOURCES 8 uint16_t mode_particleimpact(void) { if (SEGLEN == 1) @@ -8794,32 +8914,32 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled (todo: if ESP8266 is ok with out of bounds particles, this can be removed, it just takes care of the kill out of bounds setting) + PSsettings meteorsettings;// = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled + meteorsettings.asByte = 0b00101000; + //uint8_t *settingsPtr = reinterpret_cast(&meteorsettings); // access settings as one byte (wmore efficient in code and speed) + //*settingsPtr = 0b00101000; // PS settings for meteors: bounceY and gravity enabled if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) - PartSys->enableGravity(true); - PartSys->setBounceY(true); //always use ground bounce - // PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + PartSys->setGravity(); //enable default gravity + PartSys->setBounceY(true); //always use ground bounce + MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) - { - PartSys->sources[i].vx = 0; //emit speed in x - PartSys->sources[i].source.y = 10; + { + PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched - PartSys->sources[i].source.sat = 255; // full saturation, color chosen by palette } } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } @@ -8829,7 +8949,7 @@ uint16_t mode_particleimpact(void) PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness - MaxNumMeteors = min(PartSys->numSources, (uint8_t)8); + MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8851,12 +8971,12 @@ uint16_t mode_particleimpact(void) } else // speed is zero, explode! { - PartSys->sources[i].source.vy = 125; // set source speed positive so it goes into timeout and launches again + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again #ifdef ESP8266 emitparticles = random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else - emitparticles = random16(SEGMENT.intensity >> 1) + 10; // defines the size of the explosion - #endif + emitparticles = map(SEGMENT.intensity, 0, 255, 10, random16(PartSys->numParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does +#endif } for (int e = emitparticles; e > 0; e--) { @@ -8869,40 +8989,44 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { - PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); - - // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) - if ((PartSys->sources[i].source.y < PS_P_RADIUS<<1) && ( PartSys->sources[i].source.vy < 0)) // reached the bottom pixel on its way down + PartSys->sources[i].source.ttl--; //note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) //move down { - PartSys->sources[i].source.vy = 0; // set speed zero so it will explode - PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (TODO: still needed? the class takes care of that) - PartSys->sources[i].source.collide = true; - #ifdef ESP8266 - PartSys->sources[i].maxLife = 130; - PartSys->sources[i].minLife = 20; - PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #else - PartSys->sources[i].maxLife = 200; - PartSys->sources[i].minLife = 50; - PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds - #endif - PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers - } - } + PartSys->applyGravity(&PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down + { + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (!!!TODO: still needed? the class takes care of that) + PartSys->sources[i].source.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 130; + PartSys->sources[i].minLife = 20; + PartSys->sources[i].source.ttl = random16(255 - (SEGMENT.speed>>1)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #else + PartSys->sources[i].maxLife = 160; + PartSys->sources[i].minLife = 50; + PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds + #endif + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + } + } + } else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top - PartSys->sources[i].source.x = random16(PartSys->maxX); + PartSys->sources[i].source.x = random(PartSys->maxX); PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed - PartSys->sources[i].source.vx = random16(30) - 15; + PartSys->sources[i].source.vx = random(30) - 15; PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 255; // long life, will explode at bottom + PartSys->sources[i].source.ttl = 2000; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide - PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) @@ -8912,7 +9036,8 @@ uint16_t mode_particleimpact(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=120,c2=125,c3=8,o1=0,o2=0,o3=1"; +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,Explosion Size,Explosion Force,Bounce,Meteors,Cylinder,Walls,Collisions;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=8,o1=0,o2=0,o3=1"; /* Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles @@ -8927,22 +9052,16 @@ uint16_t mode_particleattractor(void) return mode_static(); ParticleSystem *PartSys = NULL; uint32_t i = 0; - PSparticle *attractor; //particle pointer to the attractor - uint8_t *counters; // counters for the applied force - PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - + PSsettings sourcesettings;// = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + sourcesettings.asByte = 0b00001100; + PSparticle *attractor; //particle pointer to the attractor if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. - { - uint32_t numParticles = (calculateNumberOfParticles() * 2) / 3; // use 75% of available particles to keep FPS high (also we need an attractor particle) - if (!initParticleSystem(PartSys, numParticles)) // init, need one extra byte per used particle for force counters + { + if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed - - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.sat = 255; // set full saturation - PartSys->sources[0].source.x = PS_P_RADIUS; //start out in bottom left corner - PartSys->sources[0].source.y = PS_P_RADIUS<<1; - PartSys->sources[0].source.vx = random16(5) + 3; - PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; //move slower in y + //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.vx = -7; PartSys->sources[0].source.collide = true; // seeded particles will collide PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies #ifdef ESP8266 @@ -8952,76 +9071,194 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].maxLife = 350; // lifetime in frames PartSys->sources[0].minLife = 50; #endif - PartSys->sources[0].vx = 0; // emitting speed - PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].var = 7; // emiting variation + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setWallHardness(230); //walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; //use 3/4 of particles + uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); + PartSys->setUsedParticles(displayparticles); + // set pointers + //attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); + attractor = &PartSys->particles[lastusedparticle + 1]; + if(SEGMENT.call == 0) + { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + //set attractor properties + if (SEGMENT.check2) + { + if((SEGMENT.call % 3) == 0) // move slowly + { + attractor->ttl = 100; //must be alive to move + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } + } + else{ + attractor->x = PartSys->maxX >> 1; // set to center + attractor->y = PartSys->maxY >> 1; + } + if (SEGMENT.call % 5 == 0) + { + PartSys->sources[0].source.hue++; + PartSys->sources[0].source.ttl = 100; //spray never dies + } + SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); + //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + else + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well + //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); + // apply force + for(i = 0; i < displayparticles; i++) + { + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. + } + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; + + +/* +Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles +uses inverse square law like in planetary motion +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +/* +uint16_t mode_particleattractor(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + uint32_t i = 0; + PSparticle *attractor; // particle pointer to the attractor + uint8_t *counters; // counters for the applied force + PSsettings sourcesettings = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSsettings sourcesettings; + uint8_t *settingsPtr = reinterpret_cast(&sourcesettings); // access settings as one byte (wmore efficient in code and speed) + *settingsPtr = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + + if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + { + if (!initParticleSystem(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) + return mode_static(); // allocation failed; //allocation failed + + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.x = PS_P_RADIUS; // start out in bottom left corner + PartSys->sources[0].source.y = PS_P_RADIUS << 1; + PartSys->sources[0].source.vx = random16(5) + 3; + PartSys->sources[0].source.vy = PartSys->sources[0].source.vx - 2; // move slower in y + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.ttl = 100; // is replenished below, it never dies +#ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; +#else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; +#endif + PartSys->sources[0].vx = 0; // emitting speed + PartSys->sources[0].vy = 0; // emitting speed + PartSys->sources[0].var = 7; // emiting variation + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(230); // walls are always same hardness + PartSys->setColorByAge(SEGMENT.check1); + + if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - - uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; + + uint16_t lastusedparticle = (PartSys->numParticles * 2) / 3; //only use 2/3 of the available particles to keep things fast uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); // set pointers attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); - counters = reinterpret_cast(PartSys->PSdataEnd); - //set attractor properties - if(SEGMENT.check2) //move attractor + // set attractor properties + if (SEGMENT.check2) // move attractor { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor } - else{ - attractor->vx = 0; // not moving - attractor->vy = 0; + else + { attractor->x = PartSys->maxX >> 1; // center attractor->y = PartSys->maxY >> 1; } - + if (SEGMENT.call % 5 == 0) { - PartSys->sources[0].source.hue++; - PartSys->sources[0].source.ttl = 100; //spray never dies + PartSys->sources[0].source.hue++; + PartSys->sources[0].source.ttl = 100; // spray never dies } - SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // emit at 180° as well + SEGMENT.aux1 = 0;//++; //line attractor angle // apply force - for(i = 0; i < displayparticles; i++) + if(SEGMENT.call % 2 == 0) + for (i = 0; i < displayparticles; i++) { - PartSys->attract(&PartSys->particles[i], attractor, counters, SEGMENT.speed, SEGMENT.check3); + //PartSys->lineAttractor(&PartSys->particles[i], attractor, SEGMENT.aux1, &counters[i], SEGMENT.speed); //TODO: upate this to advanced particles!!! } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source - - PartSys->update(); // update and render + Serial.print("vx:"); + Serial.print(attractor->vx); + Serial.print("vy:"); + Serial.print(attractor->vy); + Serial.print("x:"); + Serial.print(attractor->x); + Serial.print("y:"); + Serial.println(attractor->y); + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; - +*/ /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -9033,30 +9270,25 @@ uint16_t mode_particlespray(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint8_t numSprays; - uint32_t i; + //uint8_t numSprays; const uint8_t hardness = 200; //collision hardness is fixed - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init, no additional data needed + if (!initParticleSystem(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.sat = 255; // set full saturation - PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly - PartSys->sources[i].maxLife = 300; // lifetime in frames - PartSys->sources[i].minLife = 100; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].var = 7; - } + PartSys->setMotionBlur(200); // anable motion blur + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].maxLife = 300; // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 7; + } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { @@ -9069,34 +9301,24 @@ uint16_t mode_particlespray(void) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); - PartSys->enableGravity(SEGMENT.check1); - numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + PartSys->setGravity(8 * SEGMENT.check1); //enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint8_t percycle = numSprays; // maximum number of particles emitted per cycle - // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source + { + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - PartSys->sources[i].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[i].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - } + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - uint8_t j = 0; - for (i = 0; i < percycle; i++) - { // spray[j].source.hue = random16(); //set random color for each particle (using palette) - PartSys->angleEmit(PartSys->sources[j], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); - j = (j + 1) % numSprays; - } + PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } PartSys->update(); // update and render @@ -9118,20 +9340,20 @@ uint16_t mode_particleGEQ(void) ParticleSystem *PartSys = NULL; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys)) // init + if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else - PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS if (PartSys == NULL) { - Serial.println("ERROR: paticle system not found, nullpointer"); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } uint32_t i; @@ -9142,7 +9364,7 @@ uint16_t mode_particleGEQ(void) PartSys->setBounceY(SEGMENT.check3); PartSys->enableParticleCollisions(false); PartSys->setWallHardness(SEGMENT.custom2); - PartSys->enableGravity(true, SEGMENT.custom3<<1); //set gravity strength + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9166,7 +9388,7 @@ uint16_t mode_particleGEQ(void) for (bin = 0; bin < 16; bin++) { uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band - uint8_t emitspeed = 5 + (((uint32_t)fftResult[bin]*(uint32_t)SEGMENT.speed)>>9); // emit speed according to loudness of band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) emitparticles = 0; if (fftResult[bin] > threshold) @@ -9186,14 +9408,13 @@ uint16_t mode_particleGEQ(void) { if (PartSys->particles[i].ttl == 0) // find a dead particle { - //set particle properties - PartSys->particles[i].ttl = map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - PartSys->particles[i].y = 0; //start at the bottom - PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation + PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? PartSys->particles[i].vy = emitspeed; - PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin - PartSys->particles[i].sat = 255; // set saturation + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; } i++; @@ -9203,8 +9424,97 @@ uint16_t mode_particleGEQ(void) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,c3=31,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; + +/* +Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2200 //32767 means 180° +uint16_t mode_particlghostrider(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + PSsettings ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = random16(PartSys->maxX); + PartSys->sources[0].source.y = random16(PartSys->maxY); + SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); //angle increment + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + if(SEGMENT.intensity > 0) //spiraling + { + if(SEGMENT.aux1) + { + SEGMENT.step += SEGMENT.intensity>>3; + if((int32_t)SEGMENT.step > MAXANGLESTEP) + SEGMENT.aux1 = 0; + } + else + { + SEGMENT.step -= SEGMENT.intensity>>3; + if((int32_t)SEGMENT.step < -MAXANGLESTEP) + SEGMENT.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + //PartSys->setColorByAge(SEGMENT.check1); + PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only + + //color by age (PS always starts with hue = 255 so cannot use that) + if(SEGMENT.check1) + { + for(int i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + + //enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment + uint16_t emitangle = SEGMENT.aux0 + 32767; //+180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + int8_t newvx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; + int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; //source never dies + PartSys->particleMoveUpdate(PartSys->sources[0].source, ghostsettings); + //set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = PartSys->sources[0].source.ttl; + PartSys->particles[PartSys->usedParticles-1].sat = 0; + //emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; /* * Particle rotating GEQ @@ -9236,10 +9546,10 @@ uint16_t mode_particlecenterGEQ(void) // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes uint32_t dataSize = sizeof(PSparticle) * numParticles; dataSize += sizeof(PSsource) * (numSprays); - if (!SEGENV.allocateData(dataSize)) + if (!SEGMENT.allocateData(dataSize)) return mode_static(); // allocation failed; //allocation failed - spray = reinterpret_cast(SEGENV.data); + spray = reinterpret_cast(SEGMENT.data); // calculate the end of the spray data and assign it as the data pointer for the particles: particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer @@ -9533,8 +9843,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + //addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); @@ -9590,6 +9900,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particlghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX.h b/wled00/FX.h index bfdee58173..03c15f6825 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -59,13 +59,12 @@ /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) -//#define FRAMETIME _frametime #define FRAMETIME strip.getFrameTime() /* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 12 + #define MAX_NUM_SEGMENTS 16 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA 5120 #else @@ -73,11 +72,7 @@ #define MAX_NUM_SEGMENTS 32 #endif #if defined(ARDUINO_ARCH_ESP32S2) - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1024 // 32k by default - #else - #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default - #endif + #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default (S2 is short on free RAM) #else #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1280 // 40k by default #endif @@ -187,7 +182,7 @@ #define FX_MODE_LIGHTNING 57 #define FX_MODE_ICU 58 #define FX_MODE_MULTI_COMET 59 -#define FX_MODE_DUAL_LARSON_SCANNER 60 +#define FX_MODE_DUAL_LARSON_SCANNER 60 // candidate for removal (use Scanner with with check 1) #define FX_MODE_RANDOM_CHASE 61 #define FX_MODE_OSCILLATE 62 #define FX_MODE_PRIDE_2015 63 @@ -331,7 +326,8 @@ #define FX_MODE_PARTICLESPRAY 197 #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 -#define MODE_COUNT 200 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define MODE_COUNT 201 typedef enum mapping1D2D { M12_Pixels = 0, @@ -592,12 +588,14 @@ typedef struct Segment { inline void setPixelColor(unsigned n, uint32_t c) { setPixelColor(int(n), c); } inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } inline void setPixelColor(int n, CRGB c) { setPixelColor(n, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS void setPixelColor(float i, uint32_t c, bool aa = true); inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } + #endif uint32_t getPixelColor(int i); // 1D support functions (some implement 2D as well) - void blur(uint8_t); + void blur(uint8_t, bool smear = false); void fill(uint32_t c); void fade_out(uint8_t r); void fadeToBlackBy(uint8_t fadeBy); @@ -620,10 +618,12 @@ typedef struct Segment { inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColorXY(int(x), int(y), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS void setPixelColorXY(float x, float y, uint32_t c, bool aa = true); inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } - uint32_t getPixelColorXY(uint16_t x, uint16_t y); + #endif + uint32_t getPixelColorXY(int x, int y); // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } @@ -632,8 +632,8 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) - void blurRow(uint16_t row, fract8 blur_amount); - void blurCol(uint16_t col, fract8 blur_amount); + void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); + void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); void moveX(int8_t delta, bool wrap = false); void moveY(int8_t delta, bool wrap = false); void move(uint8_t dir, uint8_t delta, bool wrap = false); @@ -652,11 +652,14 @@ typedef struct Segment { #else inline uint16_t XY(uint16_t x, uint16_t y) { return x; } inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + #ifdef WLED_USE_AA_PIXELS inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + #endif inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } @@ -665,8 +668,8 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } inline void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} - inline void blurRow(uint16_t row, fract8 blur_amount) {} - inline void blurCol(uint16_t col, fract8 blur_amount) {} + inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} + inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} inline void moveX(int8_t delta, bool wrap = false) {} inline void moveY(int8_t delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 7aecd22716..e14b68f4f1 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -65,9 +65,10 @@ void WS2812FX::setUpMatrix() { customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; + if (customMappingTable) delete[] customMappingTable; + customMappingTable = new uint16_t[getLengthTotal()]; - if (customMappingTable != nullptr) { + if (customMappingTable) { customMappingSize = getLengthTotal(); // fill with empty in case we don't fill the entire matrix @@ -109,11 +110,11 @@ void WS2812FX::setUpMatrix() { releaseJSONBufferLock(); } - uint16_t x, y, pix=0; //pixel + unsigned x, y, pix=0; //pixel for (size_t pan = 0; pan < panel.size(); pan++) { Panel &p = panel[pan]; - uint16_t h = p.vertical ? p.height : p.width; - uint16_t v = p.vertical ? p.width : p.height; + unsigned h = p.vertical ? p.height : p.width; + unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ for(size_t i = 0; i < h; i++) { y = (p.vertical?p.rightStart:p.bottomStart) ? v-j-1 : j; @@ -138,7 +139,7 @@ void WS2812FX::setUpMatrix() { DEBUG_PRINTLN(); #endif } else { // memory allocation error - DEBUG_PRINTLN(F("Ledmap alloc error.")); + DEBUG_PRINTLN(F("ERROR 2D LED map allocation error.")); isMatrix = false; panels = 0; panel.clear(); @@ -162,8 +163,8 @@ void WS2812FX::setUpMatrix() { // XY(x,y) - gets pixel index within current segment (often used to reference leds[] array element) uint16_t IRAM_ATTR Segment::XY(uint16_t x, uint16_t y) { - uint16_t width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) - uint16_t height = virtualHeight(); // segment height in logical pixels (is always >= 1) + unsigned width = virtualWidth(); // segment width in logical pixels (can be 0 if segment is inactive) + unsigned height = virtualHeight(); // segment height in logical pixels (is always >= 1) return isActive() ? (x%width) + (y%height) * width : 0; } @@ -174,16 +175,12 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels @@ -192,7 +189,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) uint32_t tmpCol = col; for (int j = 0; j < grouping; j++) { // groupping vertically for (int g = 0; g < grouping; g++) { // groupping horizontally - uint16_t xX = (x+g), yY = (y+j); + unsigned xX = (x+g), yY = (y+j); if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end #ifndef WLED_DISABLE_MODE_BLEND @@ -217,22 +214,23 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased version of setPixelColorXY() void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) { if (!isActive()) return; // not active if (x<0.0f || x>1.0f || y<0.0f || y>1.0f) return; // not normalized - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); float fX = x * (cols-1); float fY = y * (rows-1); if (aa) { - uint16_t xL = roundf(fX-0.49f); - uint16_t xR = roundf(fX+0.49f); - uint16_t yT = roundf(fY-0.49f); - uint16_t yB = roundf(fY+0.49f); + unsigned xL = roundf(fX-0.49f); + unsigned xR = roundf(fX+0.49f); + unsigned yT = roundf(fY-0.49f); + unsigned yB = roundf(fY+0.49f); float dL = (fX - xL)*(fX - xL); float dR = (xR - fX)*(xR - fX); float dT = (fY - yT)*(fY - yT); @@ -260,14 +258,15 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) setPixelColorXY(uint16_t(roundf(fX)), uint16_t(roundf(fY)), col); } } +#endif // returns RGBW values of pixel -uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { +uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { if (!isActive()) return 0; // not active if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels if (x >= width() || y >= height()) return 0; @@ -275,92 +274,102 @@ uint32_t IRAM_ATTR Segment::getPixelColorXY(uint16_t x, uint16_t y) { } // blurRow: perform a blur on a row of a rectangular matrix -void Segment::blurRow(uint16_t row, fract8 blur_amount) { +void Segment::blurRow(uint32_t row, fract8 blur_amount, bool smear){ if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (row >= rows) return; // blur one row - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned x = 0; x < cols; x++) { - CRGB cur = getPixelColorXY(x, row); - CRGB before = cur; // remember color before blur - CRGB part = cur; - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (x>0) { - CRGB prev = CRGB(getPixelColorXY(x-1, row)) + part; - setPixelColorXY(x-1, row, prev); - } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(x, row, cur); + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (x > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(x - 1, row, prev); + } else // first pixel + setPixelColorXY(x, row, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColorXY(cols-1, row, curnew); // set last pixel } // blurCol: perform a blur on a column of a rectangular matrix -void Segment::blurCol(uint16_t col, fract8 blur_amount) { +void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active - const uint_fast16_t cols = virtualWidth(); - const uint_fast16_t rows = virtualHeight(); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); if (col >= cols) return; // blur one column - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - CRGB carryover = CRGB::Black; + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned y = 0; y < rows; y++) { - CRGB cur = getPixelColorXY(col, y); - CRGB part = cur; - CRGB before = cur; // remember color before blur - part.nscale8(seep); - cur.nscale8(keep); - cur += carryover; - if (y>0) { - CRGB prev = CRGB(getPixelColorXY(col, y-1)) + part; - setPixelColorXY(col, y-1, prev); - } - if (before != cur) // optimization: only set pixel if color has changed - setPixelColorXY(col, y, cur); - carryover = part; + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (y > 0) { + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColorXY(col, y - 1, prev); + } else // first pixel + setPixelColorXY(col, y, curnew); + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; } + setPixelColorXY(col, rows - 1, curnew); } // 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { if (!isActive() || blur_amount == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - const uint16_t dim1 = vertical ? rows : cols; - const uint16_t dim2 = vertical ? cols : rows; + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + const unsigned dim1 = vertical ? rows : cols; + const unsigned dim2 = vertical ? cols : rows; if (i >= dim2) return; const float seep = blur_amount/255.f; const float keep = 3.f - 2.f*seep; // 1D box blur CRGB tmp[dim1]; - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; - int16_t xp = vertical ? x : x-1; // "signed" to prevent underflow - int16_t yp = vertical ? y-1 : y; // "signed" to prevent underflow - uint16_t xn = vertical ? x : x+1; - uint16_t yn = vertical ? y+1 : y; + for (unsigned j = 0; j < dim1; j++) { + unsigned x = vertical ? i : j; + unsigned y = vertical ? j : i; + int xp = vertical ? x : x-1; // "signed" to prevent underflow + int yp = vertical ? y-1 : y; // "signed" to prevent underflow + unsigned xn = vertical ? x : x+1; + unsigned yn = vertical ? y+1 : y; CRGB curr = getPixelColorXY(x,y); CRGB prev = (xp<0 || yp<0) ? CRGB::Black : getPixelColorXY(xp,yp); CRGB next = ((vertical && yn>=dim1) || (!vertical && xn>=dim1)) ? CRGB::Black : getPixelColorXY(xn,yn); - uint16_t r, g, b; + unsigned r, g, b; r = (curr.r*keep + (prev.r + next.r)*seep) / 3; g = (curr.g*keep + (prev.g + next.g)*seep) / 3; b = (curr.b*keep + (prev.b + next.b)*seep) / 3; tmp[j] = CRGB(r,g,b); } - for (int j = 0; j < dim1; j++) { - uint16_t x = vertical ? i : j; - uint16_t y = vertical ? j : i; + for (unsigned j = 0; j < dim1; j++) { + unsigned x = vertical ? i : j; + unsigned y = vertical ? j : i; setPixelColorXY(x, y, tmp[j]); } } @@ -380,14 +389,14 @@ void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { // it can be used to (slowly) clear the LEDs to black. void Segment::blur1d(fract8 blur_amount) { - const uint16_t rows = virtualHeight(); + const unsigned rows = virtualHeight(); for (unsigned y = 0; y < rows; y++) blurRow(y, blur_amount); } void Segment::moveX(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= cols) return; uint32_t newPxCol[cols]; for (int y = 0; y < rows; y++) { @@ -404,8 +413,8 @@ void Segment::moveX(int8_t delta, bool wrap) { void Segment::moveY(int8_t delta, bool wrap) { if (!isActive()) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); if (!delta || abs(delta) >= rows) return; uint32_t newPxCol[rows]; for (int x = 0; x < cols; x++) { @@ -465,13 +474,13 @@ void Segment::draw_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs void Segment::fill_circle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB col) { if (!isActive() || radius == 0) return; // not active - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); - for (int16_t y = -radius; y <= radius; y++) { - for (int16_t x = -radius; x <= radius; x++) { + const int cols = virtualWidth(); + const int rows = virtualHeight(); + for (int y = -radius; y <= radius; y++) { + for (int x = -radius; x <= radius; x++) { if (x * x + y * y <= radius * radius && - int16_t(cx)+x>=0 && int16_t(cy)+y>=0 && - int16_t(cx)+x=0 && int(cy)+y>=0 && + int(cx)+x= cols || x1 >= cols || y0 >= rows || y1 >= rows) return; - const int16_t dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; + const int dx = abs(x1-x0), sx = x0dy ? dx : -dy)/2, e2; for (;;) { setPixelColorXY(x0,y0,c); if (x0==x1 && y0==y1) break; @@ -516,8 +525,8 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, if (!isActive()) return; // not active if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries - const uint16_t cols = virtualWidth(); - const uint16_t rows = virtualHeight(); + const int cols = virtualWidth(); + const int rows = virtualHeight(); const int font = w*h; CRGB col = CRGB(color); @@ -556,7 +565,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses - uint8_t xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; + unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; // calculate the intensities for each affected pixel uint8_t wu[4] = {WU_WEIGHT(ix, iy), WU_WEIGHT(xx, iy), WU_WEIGHT(ix, yy), WU_WEIGHT(xx, yy)}; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 42e98452fb..76aaccd2fc 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -148,10 +148,11 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); + DEBUG_PRINT(F("Allocating Data")); + // DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); deallocateData(); // if the old buffer was smaller release it first if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { // not enough memory @@ -327,7 +328,7 @@ void Segment::stopTransition() { } void Segment::handleTransition() { - uint16_t _progress = progress(); + unsigned _progress = progress(); if (_progress == 0xFFFFU) stopTransition(); } @@ -412,9 +413,9 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { #endif uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { - uint32_t prog = progress(); + unsigned prog = progress(); if (prog < 0xFFFFU) { - uint32_t curBri = (useCct ? cct : (on ? opacity : 0)) * prog; + unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog); return curBri / 0xFFFFU; } @@ -423,7 +424,7 @@ uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { uint8_t IRAM_ATTR Segment::currentMode() { #ifndef WLED_DISABLE_MODE_BLEND - uint16_t prog = progress(); + unsigned prog = progress(); if (modeBlending && prog < 0xFFFFU) return _t->_modeT; #endif return mode; @@ -440,13 +441,13 @@ uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) { CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) { loadPalette(targetPalette, pal); - uint16_t prog = progress(); + unsigned prog = progress(); if (strip.paletteFade && prog < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms - uint16_t noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (int i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); + unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i=0; i_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, targetPalette, 48); targetPalette = _t->_palT; // copy transitioning/temporary palette } return targetPalette; @@ -455,18 +456,18 @@ CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, u // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? - if ((millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) { + if ((uint16_t)((uint16_t)(millis() / 1000U) - _lastPaletteChange) > randomPaletteChangeTime){ _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); - _lastPaletteChange = millis()/1000U; - _lastPaletteBlend = (uint16_t)(millis() & 0xFFFF)-512; // starts blending immediately + _lastPaletteChange = (uint16_t)(millis() / 1000U); + _lastPaletteBlend = (uint16_t)((uint16_t)millis() - 512); // starts blending immediately } // if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls) if (strip.paletteFade) { // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((millis() & 0xFFFF) - _lastPaletteBlend < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = millis(); + if ((uint16_t)((uint16_t)millis() - _lastPaletteBlend) < strip.getTransition() >> 7) return; // not yet time to fade, delay the update + _lastPaletteBlend = (uint16_t)millis(); } nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); } @@ -576,7 +577,7 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) { mode = fx; // load default values from effect string if (loadDefaults) { - int16_t sOpt; + int sOpt; sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; @@ -610,21 +611,21 @@ void Segment::setPalette(uint8_t pal) { // 2D matrix uint16_t IRAM_ATTR Segment::virtualWidth() const { - uint16_t groupLen = groupLength(); - uint16_t vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen; if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED return vWidth; } uint16_t IRAM_ATTR Segment::virtualHeight() const { - uint16_t groupLen = groupLength(); - uint16_t vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); + unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen; if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED return vHeight; } uint16_t IRAM_ATTR Segment::nrOfVStrips() const { - uint16_t vLen = 1; + unsigned vLen = 1; #ifndef WLED_DISABLE_2D if (is2D()) { switch (map1D2D) { @@ -641,9 +642,9 @@ uint16_t IRAM_ATTR Segment::nrOfVStrips() const { uint16_t IRAM_ATTR Segment::virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vW = virtualWidth(); - uint16_t vH = virtualHeight(); - uint16_t vLen = vW * vH; // use all pixels from segment + unsigned vW = virtualWidth(); + unsigned vH = virtualHeight(); + unsigned vLen = vW * vH; // use all pixels from segment switch (map1D2D) { case M12_pBar: vLen = vH; @@ -656,8 +657,8 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { return vLen; } #endif - uint16_t groupLen = groupLength(); // is always >= 1 - uint16_t vLength = (length() + groupLen - 1) / groupLen; + unsigned groupLen = groupLength(); // is always >= 1 + unsigned vLength = (length() + groupLen - 1) / groupLen; if (mirror) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } @@ -674,8 +675,8 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + int vH = virtualHeight(); // segment height in logical pixels + int vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip @@ -732,14 +733,10 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } #endif - uint16_t len = length(); + unsigned len = length(); uint8_t _bri_t = currentBri(); if (_bri_t < 255) { - byte r = scale8(R(col), _bri_t); - byte g = scale8(G(col), _bri_t); - byte b = scale8(B(col), _bri_t); - byte w = scale8(W(col), _bri_t); - col = RGBW32(r, g, b, w); + col = color_fade(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) @@ -777,6 +774,7 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) } } +#ifdef WLED_USE_AA_PIXELS // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) { @@ -788,8 +786,8 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) float fC = i * (virtualLength()-1); if (aa) { - uint16_t iL = roundf(fC-0.49f); - uint16_t iR = roundf(fC+0.49f); + unsigned iL = roundf(fC-0.49f); + unsigned iR = roundf(fC+0.49f); float dL = (fC - iL)*(fC - iL); float dR = (iR - fC)*(iR - fC); uint32_t cIL = getPixelColor(iL | (vStrip<<16)); @@ -806,9 +804,10 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) setPixelColor(iL | (vStrip<<16), col); } } else { - setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); + setPixelColor(int(roundf(fC)) | (vStrip<<16), col); } } +#endif uint32_t IRAM_ATTR Segment::getPixelColor(int i) { @@ -820,8 +819,8 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) #ifndef WLED_DISABLE_2D if (is2D()) { - uint16_t vH = virtualHeight(); // segment height in logical pixels - uint16_t vW = virtualWidth(); + unsigned vH = virtualHeight(); // segment height in logical pixels + unsigned vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); @@ -877,9 +876,9 @@ uint8_t Segment::differs(Segment& b) const { } void Segment::refreshLightCapabilities() { - uint8_t capabilities = 0; - uint16_t segStartIdx = 0xFFFFU; - uint16_t segStopIdx = 0; + unsigned capabilities = 0; + unsigned segStartIdx = 0xFFFFU; + unsigned segStopIdx = 0; if (!isActive()) { _capabilities = 0; @@ -889,7 +888,7 @@ void Segment::refreshLightCapabilities() { if (start < Segment::maxWidth * Segment::maxHeight) { // we are withing 2D matrix (includes 1D segments) for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { - uint16_t index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical + unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical if (index < 0xFFFFU) { if (segStartIdx > index) segStartIdx = index; if (segStopIdx < index) segStopIdx = index; @@ -914,7 +913,7 @@ void Segment::refreshLightCapabilities() { if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { - uint8_t aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; @@ -930,8 +929,8 @@ void Segment::refreshLightCapabilities() { */ void Segment::fill(uint32_t c) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, c); else setPixelColor(x, c); @@ -943,8 +942,8 @@ void Segment::fill(uint32_t c) { */ void Segment::fade_out(uint8_t rate) { if (!isActive()) return; // not active - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D rate = (255-rate) >> 1; float mappedRate = float(rate) +1.1f; @@ -981,8 +980,8 @@ void Segment::fade_out(uint8_t rate) { // fades all pixels to black using nscale8() void Segment::fadeToBlackBy(uint8_t fadeBy) { if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply - const uint16_t cols = is2D() ? virtualWidth() : virtualLength(); - const uint16_t rows = virtualHeight(); // will be 1 for 1D + const int cols = is2D() ? virtualWidth() : virtualLength(); + const int rows = virtualHeight(); // will be 1 for 1D for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy)); @@ -993,33 +992,43 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) { /* * blurs segment content, source: FastLED colorutils.cpp */ -void Segment::blur(uint8_t blur_amount) { +void Segment::blur(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D const unsigned cols = virtualWidth(); const unsigned rows = virtualHeight(); - for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount); // blur all rows - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount); // blur all columns + for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); // blur all rows + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); // blur all columns return; } #endif - uint8_t keep = 255 - blur_amount; + uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; - uint32_t carryover = BLACK; unsigned vlength = virtualLength(); + uint32_t carryover = BLACK; + uint32_t lastnew; + uint32_t last; + uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { uint32_t cur = getPixelColor(i); uint32_t part = color_fade(cur, seep); - cur = color_add(color_fade(cur, keep), carryover, true); + curnew = color_fade(cur, keep); if (i > 0) { - uint32_t c = getPixelColor(i-1); - setPixelColor(i-1, color_add(c, part, true)); + if (carryover) + curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + if (last != prev) // optimization: only set pixel if color has changed + setPixelColor(i - 1, prev); } - setPixelColor(i, cur); + else // first pixel + setPixelColor(i, curnew); + lastnew = curnew; + last = cur; // save original value for comparison on next iteration carryover = part; } + setPixelColor(vlength - 1, curnew); } /* @@ -1057,7 +1066,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_ // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true); - uint8_t paletteIndex = i; + unsigned paletteIndex = i; if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" @@ -1091,15 +1100,24 @@ void WS2812FX::finalizeInit(void) { //if busses failed to load, add default (fresh install, FS issue, ...) if (BusManager::getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); - const uint8_t defDataPins[] = {DATA_PINS}; - const uint16_t defCounts[] = {PIXEL_COUNTS}; - const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); - const uint8_t defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - uint16_t prevLen = 0; - for (int i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[] = {defDataPins[i]}; - uint16_t start = prevLen; - uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + const unsigned defDataPins[] = {DATA_PINS}; + const unsigned defCounts[] = {PIXEL_COUNTS}; + const unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); + const unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); + const unsigned defNumBusses = defNumPins > defNumCounts && defNumCounts > 1 && defNumPins%defNumCounts == 0 ? defNumCounts : defNumPins; + const unsigned pinsPerBus = defNumPins / defNumBusses; + unsigned prevLen = 0; + for (unsigned i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { + uint8_t defPin[5]; // max 5 pins + for (unsigned j = 0; j < pinsPerBus; j++) defPin[j] = defDataPins[i*pinsPerBus + j]; + // when booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), etc + if (pinManager.isPinAllocated(defPin[0])) { + defPin[0] = 1; // start with GPIO1 and work upwards + while (pinManager.isPinAllocated(defPin[0]) && defPin[0] < WLED_NUM_PINS) defPin[0]++; + } + unsigned start = prevLen; + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); if (BusManager::add(defCfg) == -1) break; @@ -1115,7 +1133,7 @@ void WS2812FX::finalizeInit(void) { _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired(); - uint16_t busEnd = bus->getStart() + bus->getLength(); + unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; #ifdef ESP8266 if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; @@ -1159,15 +1177,19 @@ void WS2812FX::service() { if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; - uint16_t delay = FRAMETIME; + unsigned delay = FRAMETIME; if (!seg.freeze) { //only run effect function if not frozen + int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based) _virtualSegmentLength = seg.virtualLength(); //SEGLEN _colors_t[0] = gamma32(seg.currentColor(0)); _colors_t[1] = gamma32(seg.currentColor(1)); _colors_t[2] = gamma32(seg.currentColor(2)); seg.currentPalette(_currentPalette, seg.palette); // we need to pass reference - if (!cctFromRgb || correctWB) BusManager::setSegmentCCT(seg.currentBri(true), correctWB); + // when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio + // when cctFromRgb is true we implicitly calculate WW and CW from RGB values + if (cctFromRgb) BusManager::setSegmentCCT(-1); + else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); // Effect blending // When two effects are being blended, each may have different segment data, this // data needs to be saved first and then restored before running previous mode. @@ -1182,7 +1204,7 @@ void WS2812FX::service() { Segment::modeBlend(true); // set semaphore seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) _virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed) - uint16_t d2 = (*_mode[tmpMode])(); // run old mode + unsigned d2 = (*_mode[tmpMode])(); // run old mode seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) delay = MIN(delay,d2); // use shortest delay Segment::modeBlend(false); // unset semaphore @@ -1190,20 +1212,19 @@ void WS2812FX::service() { #endif seg.call++; if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition + BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } seg.next_time = nowUp + delay; } -// if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); _segment_index++; } _virtualSegmentLength = 0; - BusManager::setSegmentCCT(-1); _isServicing = false; _triggered = false; #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow effects.")); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif if (doShow) { yield(); @@ -1211,7 +1232,7 @@ void WS2812FX::service() { show(); } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTLN(F("Slow strip.")); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif } @@ -1358,13 +1379,13 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { } uint16_t WS2812FX::getLengthTotal(void) { - uint16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D + unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } uint16_t WS2812FX::getLengthPhysical(void) { - uint16_t len = 0; + unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses @@ -1390,11 +1411,7 @@ bool WS2812FX::hasCCTBus(void) { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; - switch (bus->getType()) { - case TYPE_ANALOG_5CH: - case TYPE_ANALOG_2CH: - return true; - } + if (bus->hasCCT()) return true; } return false; } @@ -1425,31 +1442,12 @@ void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t group appendSegment(Segment(0, strip.getLengthTotal())); segId = getSegmentsNum()-1; // segments are added at the end of list } -/* - if (_queuedChangesSegId == segId) _queuedChangesSegId = 255; // cancel queued change if already queued for this segment - - if (segId < getMaxSegments() && segId == getCurrSegmentId() && isServicing()) { // queue change to prevent concurrent access - // queuing a change for a second segment will lead to the loss of the first change if not yet applied - // however this is not a problem as the queued change is applied immediately after the effect function in that segment returns - _qStart = i1; _qStop = i2; _qStartY = startY; _qStopY = stopY; - _qGrouping = grouping; _qSpacing = spacing; _qOffset = offset; - _queuedChangesSegId = segId; - DEBUG_PRINT(F("Segment queued: ")); DEBUG_PRINTLN(segId); - return; // queued changes are applied immediately after effect function returns - } -*/ suspend(); _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); resume(); if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector } -/* -void WS2812FX::setUpSegmentFromQueuedChanges() { - if (_queuedChangesSegId >= getSegmentsNum()) return; - _segments[_queuedChangesSegId].setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); - _queuedChangesSegId = 255; -} -*/ + void WS2812FX::resetSegments() { _segments.clear(); // destructs all Segment as part of clearing #ifndef WLED_DISABLE_2D @@ -1464,8 +1462,8 @@ void WS2812FX::resetSegments() { void WS2812FX::makeAutoSegments(bool forceReset) { if (autoSegments) { //make one segment per bus - uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; - uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; + unsigned segStarts[MAX_NUM_SEGMENTS] = {0}; + unsigned segStops [MAX_NUM_SEGMENTS] = {0}; size_t s = 0; #ifndef WLED_DISABLE_2D @@ -1670,19 +1668,29 @@ bool WS2812FX::deserializeMap(uint8_t n) { return false; // if file does not load properly then exit } - DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + JsonObject root = pDoc->as(); + // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) + if (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { + Segment::maxWidth = min(max(root[F("width")].as(), 1), 128); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + } - if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; + if (customMappingTable) delete[] customMappingTable; + customMappingTable = new uint16_t[getLengthTotal()]; - JsonObject root = pDoc->as(); - JsonArray map = root[F("map")]; - if (!map.isNull() && map.size()) { // not an empty map - customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal()); - for (unsigned i=0; i 0); } uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) { diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 1cd99e41c0..64fee78ccc 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,11 +33,9 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not -add an x/y struct, do particle rendering using that, much easier to read - -extend rendering to more than 2x2, 3x2 (fire) should be easy, 3x3 maybe also doable without using much math (need to see if it looks good) - */ // sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public #include "FXparticleSystem.h" @@ -45,19 +43,39 @@ #include "FastLED.h" #include "FX.h" -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { - Serial.println("PS Constructor"); + //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - updatePSpointers(); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default emitIndex = 0; + + //initialize some default non-zero values most FX use + for (int i = 0; i < numSources; i++) + { + sources[i].source.sat = 255; //set saturation to max by default + } + for (int i = 0; i < numParticles; i++) + { + particles[i].sat = 255; // full saturation + } /* Serial.println("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + aliveparticles++; + } + Serial.println(aliveparticles); for (int i = 0; i < numParticles; i++) { //particles[i].ttl = 0; //initialize all particles to dead @@ -69,15 +87,16 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero Serial.println(particles[i].y); } }*/ - Serial.println("PS Constructor done"); + // Serial.println("PS Constructor done"); } //update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { + PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) - applyGravity(particles, usedParticles, gforce, &gforcecounter); + applyGravity(); // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) @@ -86,16 +105,30 @@ void ParticleSystem::update(void) //move all particles for (int i = 0; i < usedParticles; i++) { - particleMoveUpdate(particles[i], particlesettings); - } - + if(advPartProps) + { + advprop = &advPartProps[i]; + } + particleMoveUpdate(particles[i], particlesettings, advprop); + } + /*!!! remove this + Serial.print("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + if(particles[i].ttl) + aliveparticles++; + } + Serial.println(aliveparticles); + */ ParticleSys_render(); } //update function for fire animation -void ParticleSystem::updateFire(uint32_t intensity) +void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { - fireParticleupdate(); + if(!renderonly) + fireParticleupdate(); ParticleSys_render(true, intensity); } @@ -109,6 +142,11 @@ void ParticleSystem::setWallHardness(uint8_t hardness) wallHardness = hardness; } +void ParticleSystem::setWallRoughness(uint8_t roughness) +{ + wallRoughness = roughness; +} + void ParticleSystem::setCollisionHardness(uint8_t hardness) { collisionHardness = hardness; @@ -152,13 +190,29 @@ void ParticleSystem::setColorByAge(bool enable) particlesettings.colorByAge = enable; } -// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable -// if enabled, gravity is applied to all particles in ParticleSystemUpdate() -void ParticleSystem::enableGravity(bool enable, uint8_t force) +void ParticleSystem::setMotionBlur(uint8_t bluramount) +{ + if(particlesize == 0) //only allwo motion blurring on default particle size + motionBlur = bluramount; +} + +// render size using smearing (see blur function) +void ParticleSystem::setParticleSize(uint8_t size) { - particlesettings.useGravity = enable; - if (force > 0) + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; //note: this sets size if not using advanced props + motionBlur = 0; //disable motion blur if particle size is set +} +// enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem::setGravity(int8_t force) +{ + if (force) + { gforce = force; + particlesettings.useGravity = true; + } else particlesettings.useGravity = false; } @@ -184,9 +238,11 @@ void ParticleSystem::sprayEmit(PSsource &emitter) particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; break; } /* @@ -229,17 +285,23 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, uint32_t speed) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) { - emitter.vx = ((int32_t)cos16(angle) * speed) / 32767; // cos16() and sin16() return signed 16bit - emitter.vy = ((int32_t)sin16(angle) * speed) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); + /* + Serial.print(" x: "); + Serial.print(emitter.vx); + Serial.print(" y: "); + Serial.println(emitter.vy);*/ } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties) { + if (part.ttl > 0) { // age @@ -247,204 +309,213 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options) if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + bool usesize = false; //particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; int32_t newY = part.y + (int16_t)part.vy; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of vew + if(advancedproperties) //may be using individual particle size + { + if(advancedproperties->size > 0) + usesize = true; //note: variable eases out of frame checking below + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + advancedproperties->size); + } + //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options.bounceX) { - if ((newX < PS_P_RADIUS) || (newX > maxX - PS_P_RADIUS)) // reached a wall - { - part.vx = -part.vx; // invert speed - part.vx = (part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (newX < PS_P_RADIUS) - newX = PS_P_RADIUS; // fast particles will never reach the edge if position is inverted - else - newX = maxX - PS_P_RADIUS; - } + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + bounce(part.vx, part.vy, newX, maxX); } - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { if (options.wrapX) { - newX = wraparound(newX, maxX); + newX = (uint16_t)newX % (maxX + 1); } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { - part.outofbounds = 1; - if (options.killoutofbounds) - part.ttl = 0; + bool isleaving = true; + if(usesize) //using individual particle size + { + if (((newX > -particleHardRadius) || (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + + if(isleaving) + { + part.outofbounds = 1; + if (options.killoutofbounds) + part.ttl = 0; + } } } if (options.bounceY) { - if ((newY < PS_P_RADIUS) || (newY > maxY - PS_P_RADIUS)) // reached floor / ceiling + if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { - if (newY < PS_P_RADIUS) // bounce at bottom - { - part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = PS_P_RADIUS; - } + if (newY < particleHardRadius) // bounce at bottom + bounce(part.vy, part.vx, newY, maxY); else { + /* + //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX - { + { if (newY > maxY + PS_P_HALFRADIUS) part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) } - else + else*/ + if(!options.useGravity) { - part.vy = -part.vy; // invert speed - part.vy = (part.vy * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - newY = maxY - PS_P_RADIUS; + bounce(part.vy, part.vx, newY, maxY); } } } } - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { - if (options.wrapY) + if (options.wrapY) { - newY = wraparound(newY, maxY); + newY = (uint16_t)newY % (maxY + 1); } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - part.outofbounds = 1; - if (options.killoutofbounds) + { + bool isleaving = true; + if(usesize) //using individual particle size { - if (newY < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options.useGravity) - part.ttl = 0; + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) //still withing rendering reach + isleaving = false; + } + if(isleaving) + { + part.outofbounds = 1; + if (options.killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options.useGravity) + part.ttl = 0; + } } } - } - part.x = (int16_t)newX; // set new position part.y = (int16_t)newY; // set new position } } -// apply a force in x,y direction to particles -// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +//function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) +{ + incomingspeed = -incomingspeed; // invert speed + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted + else + position = maxposition - particleHardRadius; + if(wallRoughness) + { + //transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = ((random(donatespeed << 1) - donatespeed) * wallRoughness) / 255; //take random portion of + or - x speed, scaled by roughness + parallelspeed += donatespeed; + donatespeed = abs(donatespeed); + incomingspeed -= incomingspeed > 0 ? donatespeed : -donatespeed; + } + +} + +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) -void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter) +void ParticleSystem::applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter) { // for small forces, need to use a delay counter uint8_t xcounter = (*counter) & 0x0F; // lower four bits uint8_t ycounter = (*counter) >> 4; // upper four bits // velocity increase - int32_t dvx = calcForce_dV(xforce, &xcounter); - int32_t dvy = calcForce_dV(yforce, &ycounter); + int32_t dvx = calcForce_dv(xforce, &xcounter); + int32_t dvy = calcForce_dv(yforce, &ycounter); // save counter values back - *counter |= xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + *counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits *counter |= (ycounter << 4) & 0xF0; // write upper four bits - // apply the force to particle: - int32_t i = 0; - if (dvx != 0) - { - if (numparticles == 1) // for single particle, skip the for loop to make it faster - { - part[0].vx = part[0].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vx + dvx; // limit the force, this is faster than min or if/else - } - else - { - for (i = 0; i < numparticles; i++) - { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty - part[i].vx = part[i].vx + dvx > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + dvx; - } - } - } - if (dvy != 0) - { - if (numparticles == 1) // for single particle, skip the for loop to make it faster - part[0].vy = part[0].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[0].vy + dvy; - else - { - for (i = 0; i < numparticles; i++) - { - part[i].vy = part[i].vy + dvy > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + dvy; - } - } - } + // apply the force to particle: + part->vx = limitSpeed((int32_t)part->vx + dvx); + part->vy = limitSpeed((int32_t)part->vy + dvy); +} + +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) +{ + if (advPartProps == NULL) + return; // no advanced properties available + applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); } -// apply a force in x,y direction to particles directly (no counter required but no 'sub 1' force supported) -void ParticleSystem::applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce) +// apply a force in x,y direction to all particles +void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { - //note: could make this faster for single particles by adding an if statement, but it is fast enough as is - for (uint i = 0; i < numparticles; i++) + // for small forces, need to use a delay counter + uint8_t tempcounter; + + //note: this is not the most compuatationally effeicient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; i++) { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster so no speed penalty - part[i].vx = part[i].vx + xforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vx + xforce; - part[i].vy = part[i].vy + yforce > PS_P_MAXSPEED ? PS_P_MAXSPEED : part[i].vy + yforce; + tempcounter = forcecounter; + applyForce(&particles[i], xforce, yforce, &tempcounter); } + forcecounter = tempcounter; //save value back } -// apply a force in angular direction to group of particles //TODO: actually test if this works as expected, this is untested code -// caller needs to provide a 8bit counter that holds its value between calls for each group (numparticles can be 1 for single particle) +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower - // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) - applyForce(part, numparticles, xforce, yforce, counter); + applyForce(part, xforce, yforce, counter); } -// apply a force in angular direction to particles directly (no counter required but no 'sub 1' force supported) +void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) +{ + if (advPartProps == NULL) + return; // no advanced properties available + applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); +} +// apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle) +void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) { int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(part, numparticles, xforce, yforce); + applyForce(xforce, yforce); } -// apply gravity to a group of particles -// faster than apply force since direction is always down and counter is fixed for all particles -// caller needs to provide a 8bit counter that holds its value between calls -// force is in 4.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results), force above 127 are VERY strong -void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter) -{ - int32_t dv; // velocity increase - if (force > 15) - dv = (force >> 4); // apply the 4 MSBs - else - dv = 1; - - *counter += force; - - if (*counter > 15) - { - *counter -= 16; - // apply force to all used particles - for (uint32_t i = 0; i < numarticles; i++) - { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways - particles[i].vy = particles[i].vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : particles[i].vy - dv; // limit the force, this is faster than min or if/else - } - } -} +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) -//apply gravity using PS global gforce -void ParticleSystem::applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter) +// apply gravity to all particles using PS global gforce setting +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem::applyGravity() { - applyGravity(part, numarticles, gforce, counter); + int32_t dv = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); + } } //apply gravity to single particle using system settings (use this for sources) +//function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { int32_t dv; // velocity increase @@ -454,16 +525,16 @@ void ParticleSystem::applyGravity(PSparticle *part) dv = 1; if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = part->vy - dv > PS_P_MAXSPEED ? PS_P_MAXSPEED : part->vy - dv; // limit the force, this is faster than min or if/else + { + part->vy = limitSpeed((int32_t)part->vy - dv); } } -// slow down particles by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) { int32_t friction = 255 - coefficient; - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster + // note: not checking if particle is dead can be done by caller (or can be omitted) // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. part->vx = ((int16_t)part->vx * friction) / 255; part->vy = ((int16_t)part->vy * friction) / 255; @@ -472,68 +543,152 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) // apply friction to all particles void ParticleSystem::applyFriction(uint8_t coefficient) { - int32_t friction = 255 - coefficient; for (uint32_t i = 0; i < usedParticles; i++) { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is faster - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. - particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; - particles[i].vy = ((int16_t)particles[i].vy * friction) / 255; + if(particles[i].ttl) + applyFriction(&particles[i], coefficient); } } // attracts a particle to an attractor particle using the inverse square-law -void ParticleSystem::attract(PSparticle *part, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow) +void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { + if (advPartProps == NULL) + return; // no advanced properties available + // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - part->x; - int32_t dy = attractor->y - part->y; + int32_t dx = attractor->x - particles[particleindex].x; + int32_t dy = attractor->y - particles[particleindex].y; // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy + 1; + int32_t distanceSquared = dx * dx + dy * dy; if (distanceSquared < 8192) { if (swallow) // particle is close, age it fast so it fades out, do not attract further { - if (part->ttl > 7) - part->ttl -= 8; + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; else { - part->ttl = 0; + particles[particleindex].ttl = 0; return; } } - distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces } int32_t force = ((int32_t)strength << 16) / distanceSquared; int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(part, 1, xforce, yforce, counter); + applyForce(particleindex, xforce, yforce); } +//attract to a line (TODO: this is not yet working) +void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) +{ + // Calculate the distance between the particle and the attractor + if(advPartProps == NULL) + return; //no advanced properties available + + //calculate a second point on the line + int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); + int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); + //calculate squared distance from particle to the line: + int32_t dx = (x1 - attractorcenter->x) >> 4; + int32_t dy = (y1 - attractorcenter->y) >> 4; + int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; + int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); + + + // Calculate the force based on inverse square law + if (distanceSquared < 2) + { + distanceSquared = 1; + // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; + //apply force in a 90° angle to the line + int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting + int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! +/* + Serial.print(" partx: "); + Serial.print(particles[particleindex].x); + Serial.print(" party "); + Serial.print(particles[particleindex].y); + Serial.print(" x1 "); + Serial.print(x1); + Serial.print(" y1 "); + Serial.print(y1); + Serial.print(" dx "); + Serial.print(dx); + Serial.print(" dy "); + Serial.print(dy); + Serial.print(" d: "); + Serial.print(d); + Serial.print(" dsq: "); + Serial.print(distanceSquared); + Serial.print(" force: "); + Serial.print(force); + Serial.print(" fx: "); + Serial.print(xforce); + Serial.print(" fy: "); + Serial.println(yforce);*/ + + applyForce(particleindex, xforce, yforce); +} // render particles to the LED buffer (uses palette to render the 8bit particle color value) // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { - int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs CRGB baseRGB; - bool useLocalBuffer = true; - CRGB **colorbuffer; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) + CRGB **framebuffer = NULL; //local frame buffer + CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles uint32_t i; - uint32_t brightness; // particle brightness, fades if dying - //CRGB colorbuffer[maxXpixel+1][maxYpixel+1] = {0}; //put buffer on stack (not a good idea, can cause crashes on large segments if other function run the stack into the heap) + uint32_t brightness; // particle brightness, fades if dying + if (useLocalBuffer) { - // allocate memory for the local renderbuffer - colorbuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (colorbuffer == NULL) - useLocalBuffer = false; //render to segment pixels directly if not enough memory + // allocate empty memory for the local renderbuffer + framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); + if (framebuffer == NULL) + { + Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + uint32_t yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[x][y], motionBlur); + } + } + } + if(advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + } + } + } + + if(!useLocalBuffer) //disabled or allocation above failed + { + Serial.println("NOT using local buffer!"); + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it } - // go over particles and render them to the buffer for (i = 0; i < usedParticles; i++) { @@ -549,80 +704,56 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! - brightness > 255 ? 255 : brightness; // faster then using min() + brightness = brightness > 255 ? 255 : brightness; // faster then using min() baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); } else{ brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - if (particles[i].sat < 255) + if (particles[i].sat < 255) { CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - baseHSV.s = particles[i].sat; //desaturate + baseHSV.s = particles[i].sat; baseRGB = (CRGB)baseHSV; //convert back to RGB } } - int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking in rendering function does not work (if values persist), this is faster then setting it to 0 there - // calculate brightness values for all four pixels representing a particle using linear interpolation and calculate the coordinates of the phyiscal pixels to add the color to - renderParticle(&particles[i], brightness, pxlbrightness, pixco); - /* - //debug: check coordinates if out of buffer boundaries print out some info - for(uint32_t d; d<4; d++) + if(renderbuffer) //set buffer to zero if it exists { - if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) - { - pxlbrightness[d] = -1; //do not render - Serial.print("uncought out of bounds: x="); - Serial.print(pixco[d][0]); - Serial.print("particle x="); - Serial.print(particles[i].x); - Serial.print(" y="); - Serial.println(particles[i].y); - useLocalBuffer = false; - free(colorbuffer); // free buffer memory - } - if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) - { - pxlbrightness[d] = -1; // do not render - Serial.print("uncought out of bounds: y="); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[i].x); - Serial.print(" y="); - Serial.println(particles[i].y); - useLocalBuffer = false; - free(colorbuffer); // free buffer memory - } - }*/ - if (useLocalBuffer) + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // renderbuffer is 10x10 pixels. note: passing the buffer and setting it zero here is faster than creating a new buffer for every particle + } + + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + + } + + if(particlesize > 0) + { + if (useLocalBuffer) { - if (pxlbrightness[0] > 0) - colorbuffer[pixco[0][0]][pixco[0][1]] = fast_color_add(colorbuffer[pixco[0][0]][pixco[0][1]], baseRGB, pxlbrightness[0]); // bottom left - if (pxlbrightness[1] > 0) - colorbuffer[pixco[1][0]][pixco[1][1]] = fast_color_add(colorbuffer[pixco[1][0]][pixco[1][1]], baseRGB, pxlbrightness[1]); // bottom right - if (pxlbrightness[2] > 0) - colorbuffer[pixco[2][0]][pixco[2][1]] = fast_color_add(colorbuffer[pixco[2][0]][pixco[2][1]], baseRGB, pxlbrightness[2]); // top right - if (pxlbrightness[3] > 0) - colorbuffer[pixco[3][0]][pixco[3][1]] = fast_color_add(colorbuffer[pixco[3][0]][pixco[3][1]], baseRGB, pxlbrightness[3]); // top left + //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); + if (particlesize > 64) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); + if (particlesize > 128) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); + if (particlesize > 192) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); } else - { - SEGMENT.fill(BLACK); // clear the matrix - if (pxlbrightness[0] > 0) - SEGMENT.addPixelColorXY(pixco[0][0], maxYpixel - pixco[0][1], baseRGB.scale8((uint8_t)pxlbrightness[0])); // bottom left - if (pxlbrightness[1] > 0) - SEGMENT.addPixelColorXY(pixco[1][0], maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)pxlbrightness[1])); // bottom right - if (pxlbrightness[2] > 0) - SEGMENT.addPixelColorXY(pixco[2][0], maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)pxlbrightness[2])); // top right - if (pxlbrightness[3] > 0) - SEGMENT.addPixelColorXY(pixco[3][0], maxYpixel - pixco[3][1], baseRGB.scale8((uint8_t)pxlbrightness[3])); // top left - // test to render larger pixels with minimal effort (not working yet, need to calculate coordinate from actual dx position but brightness seems right), could probably be extended to 3x3 - // SEGMENT.addPixelColorXY(pixco[1][0] + 1, maxYpixel - pixco[1][1], baseRGB.scale8((uint8_t)((brightness>>1) - pxlbrightness[0])), fastcoloradd); - // SEGMENT.addPixelColorXY(pixco[2][0] + 1, maxYpixel - pixco[2][1], baseRGB.scale8((uint8_t)((brightness>>1) -pxlbrightness[3])), fastcoloradd); + { + SEGMENT.blur(particlesize, true); + if (particlesize > 64) + SEGMENT.blur(particlesize - 64, true); + if (particlesize > 128) + SEGMENT.blur((particlesize - 128) << 1, true); + if (particlesize > 192) + SEGMENT.blur((particlesize - 192) << 1, true); } + } - if (useLocalBuffer) + + if (useLocalBuffer) //transfer local buffer back to segment { uint32_t yflipped; for (int y = 0; y <= maxYpixel; y++) @@ -630,33 +761,50 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) yflipped = maxYpixel - y; for (int x = 0; x <= maxXpixel; x++) { - SEGMENT.setPixelColorXY(x, yflipped, colorbuffer[x][y]); + SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } } - free(colorbuffer); // free buffer memory + free(framebuffer); // free buffer memory } + if(renderbuffer) + free(renderbuffer); // free buffer memory } -// calculate pixel positions and brightness distribution for rendering function -// pixelpositions are the physical positions in the matrix that the particle renders to (4x2 array for the four positions) -void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]) +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { + int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; //rendering for advanced particles + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particle->x - PS_P_HALFRADIUS; - int32_t yoffset = particle->y - PS_P_HALFRADIUS; + int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y - PS_P_HALFRADIUS; int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space int32_t dy = yoffset % PS_P_RADIUS; int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) int32_t y = yoffset >> PS_P_RADIUS_SHIFT; + //check if particle has advanced size properties and buffer is available + if(advPartProps) + { + if(advPartProps[particleindex].size > 0) + { + if(renderbuffer) + advancedrender = true; + else + return; //cannot render without buffer, advanced size particles are allowed out of frame + } + } + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixelpositions[0][0] = pixelpositions[3][0] = x; // bottom left & top left - pixelpositions[0][1] = pixelpositions[1][1] = y; // bottom left & bottom right - pixelpositions[1][0] = pixelpositions[2][0] = x + 1; // bottom right & top right - pixelpositions[2][1] = pixelpositions[3][1] = y + 1; // top right & top left + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[3][1] = y + 1; // top right & top left // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) - + if (x < 0) // left pixels out of frame { dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) @@ -664,19 +812,19 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in //checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: if (dx == PS_P_RADIUS) { - pixelvalues[1] = pixelvalues[2] = -1; // pixel is actually out of matrix boundaries, do not render + pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render } if (particlesettings.wrapX) // wrap x to the other side if required - pixelpositions[0][0] = pixelpositions[3][0] = maxXpixel; + pixco[0][0] = pixco[3][0] = maxXpixel; else - pixelvalues[0] = pixelvalues[3] = -1; // pixel is out of matrix boundaries, do not render + pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render } - else if (pixelpositions[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow + else if (pixco[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow { if (particlesettings.wrapX) // wrap y to the other side if required - pixelpositions[1][0] = pixelpositions[2][0] = 0; + pixco[1][0] = pixco[2][0] = 0; else - pixelvalues[1] = pixelvalues[2] = -1; + pxlbrightness[1] = pxlbrightness[2] = -1; } if (y < 0) // bottom pixels out of frame @@ -684,19 +832,25 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in dy = PS_P_RADIUS + dy; //see note above if (dy == PS_P_RADIUS) { - pixelvalues[2] = pixelvalues[3] = -1; // pixel is actually out of matrix boundaries, do not render + pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render } if (particlesettings.wrapY) // wrap y to the other side if required - pixelpositions[0][1] = pixelpositions[1][1] = maxYpixel; + pixco[0][1] = pixco[1][1] = maxYpixel; else - pixelvalues[0] = pixelvalues[1] = -1; + pxlbrightness[0] = pxlbrightness[1] = -1; } - else if (pixelpositions[2][1] > maxYpixel) // top pixels + else if (pixco[2][1] > maxYpixel) // top pixels { if (particlesettings.wrapY) // wrap y to the other side if required - pixelpositions[2][1] = pixelpositions[3][1] = 0; + pixco[2][1] = pixco[3][1] = 0; else - pixelvalues[2] = pixelvalues[3] = -1; + pxlbrightness[2] = pxlbrightness[3] = -1; + } + + if(advancedrender) //always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + { + for(uint32_t i = 0; i < 4; i++) + pxlbrightness[i] = 0; } // calculate brightness values for all four pixels representing a particle using linear interpolation @@ -706,14 +860,112 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in int32_t precal3 = dy * brightess; //calculate the values for pixels that are in frame - if (pixelvalues[0] >= 0) - pixelvalues[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pixelvalues[1] >= 0) - pixelvalues[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pixelvalues[2] >= 0) - pixelvalues[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE - if (pixelvalues[3] >= 0) - pixelvalues[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + if (pxlbrightness[0] >= 0) + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[2] >= 0) + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pxlbrightness[3] >= 0) + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + + + + if(advancedrender) + { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + + //first, render the pixel to the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... + uint32_t rendersize = 4; + uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer + blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 + if (advPartProps[particleindex].size > 64) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size - 64, advPartProps[particleindex].size - 64, true, offset, offset, true); //blur to 6x6 + } + if (advPartProps[particleindex].size > 128) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 128) << 1, (advPartProps[particleindex].size - 128) << 1, true, offset, offset, true); //blur to 8x8 + } + if (advPartProps[particleindex].size > 192) + { + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 192) << 1, (advPartProps[particleindex].size - 192) << 1, true, offset, offset, true); //blur to 10x10 + } + + //calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + //transfer renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if(xfb > maxXpixel) + if (particlesettings.wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); + else + continue; + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) + { + yfb = yfb_orig + yrb; + if(yfb > maxYpixel) + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); //TODO: this is just a test, need to render to correct coordinates with out of frame checking + } + } + } + else + { + if (framebuffer) + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } + + //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? + //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... + //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. + +/* + //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); //4x4 + if (particlesize > 64) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); //6x6 + if (particlesize > 128) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); 8x8 + if (particlesize > 192) + blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); 10x10 + */ + + /* Serial.print("x:"); Serial.print(particle->x); @@ -727,62 +979,63 @@ void ParticleSystem::renderParticle(PSparticle* particle, uint32_t brightess, in for(uint8_t t = 0; t<4; t++) { Serial.print(" v"); - Serial.print(pixelvalues[t]); + Serial.print(pxlbrightness[t]); Serial.print(" x"); - Serial.print(pixelpositions[t][0]); + Serial.print(pixco[t][0]); Serial.print(" y"); - Serial.print(pixelpositions[t][1]); + Serial.print(pixco[t][1]); Serial.print(" "); } Serial.println(" "); */ + /* - // debug: check coordinates if out of buffer boundaries print out some info + // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) for (uint32_t d = 0; d < 4; d++) { - if (pixelpositions[d][0] < 0 || pixelpositions[d][0] > maxXpixel) + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) { //Serial.print("<"); - if (pixelvalues[d] >= 0) + if (pxlbrightness[d] >= 0) { Serial.print("uncought out of bounds: x:"); - Serial.print(pixelpositions[d][0]); + Serial.print(pixco[d][0]); Serial.print(" y:"); - Serial.print(pixelpositions[d][1]); + Serial.print(pixco[d][1]); Serial.print("particle x="); - Serial.print(particle->x); + Serial.print(particles[particleindex].x); Serial.print(" y="); - Serial.println(particle->y); - pixelvalues[d] = -1; // do not render + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render } } - if (pixelpositions[d][1] < 0 || pixelpositions[d][1] > maxYpixel) + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) { //Serial.print("^"); - if (pixelvalues[d] >= 0) + if (pxlbrightness[d] >= 0) { - Serial.print("uncought out of bounds: x:"); - Serial.print(pixelpositions[d][0]); + Serial.print("uncought out of bounds: y:"); + Serial.print(pixco[d][0]); Serial.print(" y:"); - Serial.print(pixelpositions[d][1]); + Serial.print(pixco[d][1]); Serial.print("particle x="); - Serial.print(particle->x); + Serial.print(particles[particleindex].x); Serial.print(" y="); - Serial.println(particle->y); - pixelvalues[d] = -1; // do not render + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render } } } */ + } // update & move particle, wraps around left/right if settings.wrapX is true, wrap around up/down if settings.wrapY is true // particles move upwards faster if ttl is high (i.e. they are hotter) void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first and check again - //todo: kill out of bounds funktioniert nicht? + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function uint32_t i = 0; for (i = 0; i < usedParticles; i++) @@ -794,7 +1047,7 @@ void ParticleSystem::fireParticleupdate() // apply velocity particles[i].x = particles[i].x + (int32_t)particles[i].vx; particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //!! shift ttl by 2 is the original value, this is experimental + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting particles[i].outofbounds = 0; // check if particle is out of bounds, wrap x around to other side if wrapping is enabled // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds @@ -809,7 +1062,7 @@ void ParticleSystem::fireParticleupdate() { if (particlesettings.wrapX) { - particles[i].x = wraparound(particles[i].x, maxX); + particles[i].x = (uint16_t)particles[i].x % (maxX + 1); } else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view { @@ -821,7 +1074,6 @@ void ParticleSystem::fireParticleupdate() } } - // detect collisions in an array of particles and handle them void ParticleSystem::handleCollisions() { @@ -829,7 +1081,6 @@ void ParticleSystem::handleCollisions() uint32_t i, j; uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) // if m ore accurate collisions are needed, just call it twice in a row if (collisioncounter & 0x01) @@ -839,7 +1090,7 @@ void ParticleSystem::handleCollisions() } collisioncounter++; - //startparticle = 0;//!!! test: do all collisions every frame, FPS goes from about 52 to + //startparticle = 0;//!!!TODO test: do all collisions every frame //endparticle = usedParticles; for (i = startparticle; i < endparticle; i++) @@ -853,10 +1104,14 @@ void ParticleSystem::handleCollisions() if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; - if (dx < PS_P_HARDRADIUS && dx > -PS_P_HARDRADIUS) // check x direction, if close, check y direction + if(advPartProps) //may be using individual particle size + { + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); //collision distance + } + if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { dy = particles[i].y - particles[j].y; - if (dy < PS_P_HARDRADIUS && dy > -PS_P_HARDRADIUS) // particles are close + if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close collideParticles(&particles[i], &particles[j]); } } @@ -885,18 +1140,14 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl dx = -1; if (relativeVx < 0) // if true, particle2 is on the right side dx = 1; - else if(relativeVx == 0) //if true - { + else if(relativeVx == 0) relativeVx = 1; - } - + dy = -1; if (relativeVy < 0) dy = 1; else if (relativeVy == 0) - { relativeVy = 1; - } distanceSquared = 2; //1 + 1 } @@ -966,8 +1217,8 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl - // const int32_t HARDDIAMETER = 2 * PS_P_HARDRADIUS; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really - // int32_t push = (2 * PS_P_HARDRADIUS * PS_P_HARDRADIUS - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + // const int32_t HARDDIAMETER = 2 * particleHardRadius; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really + // int32_t push = (2 * particleHardRadius * particleHardRadius - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push; @@ -1076,28 +1327,15 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } -//fast calculation of particle wraparound (modulo version takes 37 instructions, this only takes 28, other variants are slower on ESP8266) -//function assumes that out of bounds is checked before calling it -int32_t ParticleSystem::wraparound(int32_t p, int32_t maxvalue) -{ - if (p < 0) - { - p += maxvalue + 1; - } - else //if (p > maxvalue) - { - p -= maxvalue + 1; - } - return p; -} - //calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) //force is in 3.4 fixedpoint notation, +/-127 -int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) +int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) { + if(force == 0) + return 0; // for small forces, need to use a delay counter int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) - int32_t dv = 0; + int32_t dv; // for small forces, need to use a delay counter, apply force only if it overflows if (force_abs < 16) { @@ -1105,7 +1343,7 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) if (*counter > 15) { *counter -= 16; - dv = (force < 0) ? -1 : ((force > 0) ? 1 : 0); // force is either, 1, 0 or -1 if it is small + dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) } } else @@ -1115,12 +1353,16 @@ int32_t ParticleSystem::calcForce_dV(int8_t force, uint8_t* counter) return dv; } +//limit speed to prevent overflows +int32_t ParticleSystem::limitSpeed(int32_t speed) +{ + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); +} + // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { - cli();//!!! test to see if anything messes with the allocation (flicker issues) CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); - sei(); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else @@ -1143,126 +1385,234 @@ void ParticleSystem::updateSystem(void) // update matrix size uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - setMatrixSize(cols, rows); - updatePSpointers(); + setMatrixSize(cols, rows); + updatePSpointers(advPartProps != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers() +void ParticleSystem::updatePSpointers(bool isadvanced) { //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); - - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS + //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + //Note on memory alignment: + //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + //by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + if(isadvanced) + { + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numParticles); + } + else + { + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + advPartProps = NULL; + } + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); + //DEBUG_PRINTF_P(PSTR("adv. props %p\n"), advPartProps); //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } //non class functions to use for initialization -uint32_t calculateNumberOfParticles() +uint32_t calculateNumberOfParticles(bool isadvanced) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - uint numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel - uint particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) + uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint numberofParticles = (cols * rows); // 1 particle per pixe - uint particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint numberofParticles = (cols * rows); // 1 particle per pixel (for example 768 particles on 32x16) - uint particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint)1, min(numberofParticles, particlelimit)); + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + //make sure it is a multiple of 4 for proper memory alignment + numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } -uint32_t calculateNumberOfSources() +uint32_t calculateNumberOfSources(uint8_t requestedsources) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - int numberofSources = (cols * rows) / 8; + int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = (cols * rows) / 6; + int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else - int numberofSources = (cols * rows) / 4; - numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 72 + int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif + //make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes) +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); + //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; - Serial.print("allocating: "); - Serial.print(requiredmemory); - Serial.println("Bytes"); - Serial.print("allocating for segment at"); - Serial.println((uintptr_t)SEGMENT.data); + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool isadvanced, uint16_t additionalbytes) { - Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(); - uint32_t numsources = calculateNumberOfSources(); - if (!allocateParticleSystemMemory(numparticles, numsources, additionalbytes)) + //Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles(isadvanced); + uint32_t numsources = calculateNumberOfSources(requestedsources); + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory(numparticles, numsources, isadvanced, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; } - Serial.print("segment.data ptr"); - Serial.println((uintptr_t)(SEGMENT.data)); + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources); // particle system constructor TODO: why does VS studio thinkt this is bad? - Serial.print("PS pointer at "); - Serial.println((uintptr_t)PartSys); + //Serial.println("calling constructor"); + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, isadvanced); // particle system constructor TODO: why does VS studio thinkt this is bad? + //Serial.print("PS pointer at "); + //Serial.println((uintptr_t)PartSys); return true; } +/////////////////////// +// Utility Functions // +/////////////////////// + + // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) -CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale) +// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) { - CRGB result; - scale++; //add one to scale so 255 will not scale when shifting - uint32_t r = c1.r + ((c2.r * (scale)) >> 8); - uint32_t g = c1.g + ((c2.g * (scale)) >> 8); - uint32_t b = c1.b + ((c2.b * (scale)) >> 8); + uint32_t r, g, b; + if(scale < 255) + { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else{ + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } uint32_t max = r; - if (g > max) //note: using ? operator would be slower by 2 cpu cycles + if (g > max) //note: using ? operator would be slower by 2 instructions max = g; if (b > max) max = b; if (max < 256) { - result.r = r; - result.g = g; - result.b = b; + c1.r = r; //save result to c1 + c1.g = g; + c1.b = b; } else { - result.r = (r * 255) / max; - result.g = (g * 255) / max; - result.b = (b * 255) / max; + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; } - return result; } + +//faster than fastled color scaling as it uses a 32bit scale factor and pointer +void fast_color_scale(CRGB &c, uint32_t scale) +{ + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + + + +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) +{ + + //TODO: for particle rendering, first row and last row can be skipped in x blurring as it is all black, this would increase rendering speed + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + if(isparticle) //first and last row are always black in particle rendering + { + ystart++; + ysize--; + } + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + carryover = BLACK; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + seeppart = colorbuffer[x][y]; //create copy of current color + fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(!smear) //fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if(x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + if(isparticle) //now also do first and last row + { + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + carryover = BLACK; + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + seeppart = colorbuffer[x][y]; //create copy of current color + fast_color_scale(seeppart, seep); //scale it and seep to neighbours + if(!smear) //fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if(y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } +} \ No newline at end of file diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index af8c37d1bc..dacf6ab5b1 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,9 +30,9 @@ #include "FastLED.h" //memory allocation -#define ESP8266_MAXPARTICLES 148 // enough for one 16x16 segment with transitions +#define ESP8266_MAXPARTICLES 180// // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 -#define ESP32S2_MAXPARTICLES 768 // enough for four 16x16 segments +#define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 #define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... #define ESP32_MAXSOURCES 64 @@ -42,39 +42,73 @@ #define PS_P_HALFRADIUS 32 #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 -#define PS_P_HARDRADIUS 80 //hard surface radius of a particle, used for collision detection proximity +#define PS_P_MINHARDRADIUS 70 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 200 //maximum speed a particle can have +#define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) -//struct for a single particle +//struct for a single particle (10 bytes) typedef struct { int16_t x; //x position in particle system int16_t y; //y position in particle system int8_t vx; //horizontal velocity int8_t vy; //vertical velocity uint8_t hue; // color hue - uint8_t sat; // color saturation + uint8_t sat; //particle color saturation //two byte bit field: uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions bool flag3 : 1; // unused flags... - bool flag4 : 1; + bool flag4 : 1; } PSparticle; -//struct for a particle source +// struct for additional particle settings (optional) +typedef struct +{ + + uint8_t size; //particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; //counter for applying forces to individual particles + + //bool flag1 : 1; // unused flags... for now. + //bool flag2 : 1; + //bool flag3 : 1; + //bool flag4 : 1; +} PSadvancedParticle; + +// struct for advanced particle size control (optional) TODO: this is currently just an idea, may not make it into final code if too slow / complex +typedef struct +{ + uint8_t sizeasymmetry; // asymmetrical size TODO: need something better to define this? + uint8_t targetsize; // target size for growing / shrinking + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t sizecounter; // counter that can be used for size contol TODO: need more than one? + //ideas: + //wobbleamount, rotation angle for asymmetic particles + //a flag 'usegravity' that can be set to false for selective gravity application + + bool grow : 1; // flags + bool shrink : 1; + bool wobble : 1; + bool flag4 : 1; +} PSsizeControl; + + +//struct for a particle source (17 bytes) typedef struct { - uint16_t minLife; //minimum ttl of emittet particles - uint16_t maxLife; //maximum ttl of emitted particles - PSparticle source; //use a particle as the emitter source (speed, position, color) - uint8_t var; //variation of emitted speed - int8_t vx; //emitting speed - int8_t vy; //emitting speed -} PSsource; + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle source; // use a particle as the emitter source (speed, position, color) + uint8_t var; // variation of emitted speed (use odd numbers for good symmetry) + int8_t vx; // emitting speed + int8_t vy; + uint8_t size; // particle size (advanced property) +} PSsource; // struct for PS settings -typedef struct +typedef union { + struct{ // add a one byte bit field: bool wrapX : 1; bool wrapY : 1; @@ -84,57 +118,64 @@ typedef struct bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; //order is: LSB is first entry in the list above } PSsettings; +//class uses approximately 60 bytes class ParticleSystem { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources); // constructor + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix - void updateFire(uint32_t intensity); // update function for fire - + void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters void flameEmit(PSsource &emitter); void sprayEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, uint32_t speed); - void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions - void particleMoveUpdate(PSparticle &part, PSsettings &options); - - //particle physics - void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t force, uint8_t *counter); - void applyGravity(PSparticle *part, uint32_t numarticles, uint8_t *counter); //use global gforce - void applyGravity(PSparticle *part); //use global system settings - void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyForce(PSparticle *part, uint32_t numparticles, int8_t xforce, int8_t yforce); - void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle, uint8_t *counter); - void applyAngleForce(PSparticle *part, uint32_t numparticles, uint8_t force, uint16_t angle); + void particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties = NULL); + //particle physics + void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) + void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); + void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); //use this for advanced property particles + void applyForce(int8_t xforce, int8_t yforce); //apply a force to all particles + void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); + void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles + void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle void applyFriction(uint8_t coefficient); // apply friction to all used particles - void attract(PSparticle *particle, PSparticle *attractor, uint8_t *counter, uint8_t strength, bool swallow); - + void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); + void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); + //set options void setUsedParticles(uint16_t num); void setCollisionHardness(uint8_t hardness); //hardness for particle collisions (255 means full hard) void setWallHardness(uint8_t hardness); //hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(uint8_t roughness); //wall roughness randomizes wall collisions void setMatrixSize(uint16_t x, uint16_t y); void setWrapX(bool enable); void setWrapY(bool enable); void setBounceX(bool enable); void setBounceY(bool enable); void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die + void setSaturation(uint8_t sat); //set global color saturation void setColorByAge(bool enable); - void enableGravity(bool enable, uint8_t force = 8); + void setMotionBlur(uint8_t bluramount); + void setParticleSize(uint8_t size); + void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels - uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required) + uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -143,32 +184,43 @@ class ParticleSystem private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(PSparticle *particle, uint32_t brightess, int32_t *pixelvalues, int32_t (*pixelpositions)[2]); + void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer); //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles void handleCollisions(); void collideParticles(PSparticle *particle1, PSparticle *particle2); void fireParticleupdate(); //utility functions - void updatePSpointers(); // update the data pointers to current segment data space - int32_t wraparound(int32_t w, int32_t maxvalue); - int32_t calcForce_dV(int8_t force, uint8_t *counter); + void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall + int16_t wraparound(uint16_t p, uint32_t maxvalue); + int32_t calcForce_dv(int8_t force, uint8_t *counter); + int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - int32_t wallHardness; + uint8_t wallHardness; + uint8_t wallRoughness; uint8_t gforcecounter; //counter for global gravity - uint8_t gforce; //gravity strength, default is 8 - uint8_t collisioncounter; //counter to handle collisions + int8_t gforce; //gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? + uint8_t forcecounter; //counter for globally applied forces + //global particle properties for basic particles + uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles(); -uint32_t calculateNumberOfSources(); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, uint16_t additionalbytes); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool advanced = false, uint16_t additionalbytes = 0); +uint32_t calculateNumberOfParticles(bool advanced); +uint32_t calculateNumberOfSources(uint8_t requestedsources); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); //color add function -CRGB fast_color_add(CRGB c1, CRGB c2, uint32_t scale); // fast and accurate color adding with scaling (scales c2 before adding) \ No newline at end of file +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); \ No newline at end of file diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3ac12c04ec..ac6923f406 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -9,9 +9,10 @@ #include "bus_wrapper.h" #include "bus_manager.h" +extern bool cctICused; + //colors.cpp uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); -uint16_t approximateKelvinFromRGB(uint32_t rgb); //udp.cpp uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte *buffer, uint8_t bri=255, bool isRGBW=false); @@ -122,13 +123,13 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) } _iType = PolyBus::getI(bc.type, _pins, nr); if (_iType == I_NONE) return; - if (bc.doubleBuffer && !allocData(bc.count * (Bus::hasWhite(_type) + 3*Bus::hasRGB(_type)))) return; //warning: hardcoded channel count + if (bc.doubleBuffer && !allocData(bc.count * Bus::getNumberOfChannels(bc.type))) return; //_buffering = bc.doubleBuffer; uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus _busPtr = PolyBus::create(_iType, _pins, lenToCreate + _skip, nr, _frequencykHz); _valid = (_busPtr != nullptr); - DEBUG_PRINTF("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n", _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], _pins[1], _iType, _milliAmpsPerLed, _milliAmpsMax); + DEBUG_PRINTF_P(PSTR("%successfully inited strip %u (len %u) with type %u and pins %u,%u (itype %u). mA=%d/%d\n"), _valid?"S":"Uns", nr, bc.count, bc.type, _pins[0], IS_2PIN(bc.type)?_pins[1]:255, _iType, _milliAmpsPerLed, _milliAmpsMax); } //fine tune power estimation constants for your setup @@ -205,13 +206,15 @@ void BusDigital::show() { _milliAmpsTotal = 0; if (!_valid) return; + uint8_t cctWW = 0, cctCW = 0; uint8_t newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); + if (_data) { + size_t channels = getNumberOfChannels(); + int16_t oldCCT = Bus::_cct; // temporarily save bus CCT for (size_t i=0; i<_len; i++) { - size_t offset = i*channels; + size_t offset = i * channels; uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); uint32_t c; if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) @@ -221,17 +224,26 @@ void BusDigital::show() { case 2: c = RGBW32(_data[offset-2], _data[offset-1], _data[offset] , 0); break; } } else { - c = RGBW32(_data[offset],_data[offset+1],_data[offset+2],(Bus::hasWhite(_type)?_data[offset+3]:0)); + if (hasRGB()) c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); + else c = RGBW32(0, 0, 0, _data[offset]); + } + if (hasCCT()) { + // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT + // we need to extract and appy CCT value for each pixel individually even though all buses share the same _cct variable + // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer + Bus::_cct = _data[offset+channels-1]; + Bus::calculateCCT(c, cctWW, cctCW); } uint16_t pix = i; if (_reversed) pix = _len - pix -1; pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } #if !defined(STATUSLED) || STATUSLED>=0 if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black #endif for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black + Bus::_cct = oldCCT; } else { if (newBri < _bri) { uint16_t hwLen = _len; @@ -239,7 +251,8 @@ void BusDigital::show() { for (unsigned i = 0; i < hwLen; i++) { // use 0 as color order, actual order does not matter here as we just update the channel values as-is uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, i, 0), _bri); - PolyBus::setPixelColor(_busPtr, _iType, i, c, 0); // repaint all pixels with new brightness + if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); // this will unfortunately corrupt (segment) CCT data on every bus + PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness } } } @@ -278,17 +291,20 @@ void BusDigital::setStatusPixel(uint32_t c) { void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { if (!_valid) return; - if (Bus::hasWhite(_type)) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); - size_t offset = pix*channels; - if (Bus::hasRGB(_type)) { + uint8_t cctWW = 0, cctCW = 0; + if (hasWhite()) c = autoWhiteCalc(c); + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (_data) { + size_t offset = pix * getNumberOfChannels(); + if (hasRGB()) { _data[offset++] = R(c); _data[offset++] = G(c); _data[offset++] = B(c); } - if (Bus::hasWhite(_type)) _data[offset] = W(c); + if (hasWhite()) _data[offset++] = W(c); + // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT + // we need to store CCT value for each pixel (if there is a color correction in play, convert K in CCT ratio) + if (hasCCT()) _data[offset] = Bus::_cct >= 1900 ? (Bus::_cct - 1900) >> 5 : (Bus::_cct < 0 ? 127 : Bus::_cct); // TODO: if _cct == -1 we simply ignore it } else { if (_reversed) pix = _len - pix -1; pix += _skip; @@ -303,21 +319,21 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; } } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co); + if (hasCCT()) Bus::calculateCCT(c, cctWW, cctCW); + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); } } // returns original color if global buffering is enabled, else returns lossly restored color from bus uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { if (!_valid) return 0; - if (_data) { // use _buffering this causes ~20% FPS drop - size_t channels = Bus::hasWhite(_type) + 3*Bus::hasRGB(_type); - size_t offset = pix*channels; + if (_data) { + size_t offset = pix * getNumberOfChannels(); uint32_t c; - if (!Bus::hasRGB(_type)) { + if (!hasRGB()) { c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); } else { - c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], Bus::hasWhite(_type) ? _data[offset+3] : 0); + c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); } return c; } else { @@ -414,48 +430,31 @@ BusPwm::BusPwm(BusConfig &bc) void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel if (_type != TYPE_ANALOG_3CH) c = autoWhiteCalc(c); - if (_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { - c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { + c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } uint8_t r = R(c); uint8_t g = G(c); uint8_t b = B(c); uint8_t w = W(c); - uint8_t cct = 0; //0 - full warm white, 255 - full cold white - if (_cct > -1) { - if (_cct >= 1900) cct = (_cct - 1900) >> 5; - else if (_cct < 256) cct = _cct; - } else { - cct = (approximateKelvinFromRGB(c) - 1900) >> 5; - } - - uint8_t ww, cw; - #ifdef WLED_USE_IC_CCT - ww = w; - cw = cct; - #else - //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) - if (cct < _cctBlend) ww = 255; - else ww = ((255-cct) * 255) / (255 - _cctBlend); - - if ((255-cct) < _cctBlend) cw = 255; - else cw = (cct * 255) / (255 - _cctBlend); - - ww = (w * ww) / 255; //brightness scaling - cw = (w * cw) / 255; - #endif switch (_type) { case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation _data[0] = w; break; case TYPE_ANALOG_2CH: //warm white + cold white - _data[1] = cw; - _data[0] = ww; + if (cctICused) { + _data[0] = w; + _data[1] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; + } else { + Bus::calculateCCT(c, _data[0], _data[1]); + } break; case TYPE_ANALOG_5CH: //RGB + warm white + cold white - _data[4] = cw; - w = ww; + if (cctICused) + _data[4] = Bus::_cct < 0 || Bus::_cct > 255 ? 127 : Bus::_cct; + else + Bus::calculateCCT(c, w, _data[4]); case TYPE_ANALOG_4CH: //RGBW _data[3] = w; case TYPE_ANALOG_3CH: //standard dumb RGB @@ -467,7 +466,22 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { //does no index check uint32_t BusPwm::getPixelColor(uint16_t pix) { if (!_valid) return 0; - return RGBW32(_data[0], _data[1], _data[2], _data[3]); + // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) + switch (_type) { + case TYPE_ANALOG_1CH: //one channel (white), relies on auto white calculation + return RGBW32(0, 0, 0, _data[0]); + case TYPE_ANALOG_2CH: //warm white + cold white + if (cctICused) return RGBW32(0, 0, 0, _data[0]); + else return RGBW32(0, 0, 0, _data[0] + _data[1]); + case TYPE_ANALOG_5CH: //RGB + warm white + cold white + if (cctICused) return RGBW32(_data[0], _data[1], _data[2], _data[3]); + else return RGBW32(_data[0], _data[1], _data[2], _data[3] + _data[4]); + case TYPE_ANALOG_4CH: //RGBW + return RGBW32(_data[0], _data[1], _data[2], _data[3]); + case TYPE_ANALOG_3CH: //standard dumb RGB + return RGBW32(_data[0], _data[1], _data[2], 0); + } + return RGBW32(_data[0], _data[0], _data[0], _data[0]); } #ifndef ESP8266 @@ -506,7 +520,7 @@ void BusPwm::show() { uint8_t numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; #ifdef ESP8266 - unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri + 0.5f)); // using gamma 1.7 to extrapolate PWM duty cycle + unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle #else unsigned pwmBri = cieLUT[_bri] >> (12 - _depth); // use CIE LUT #endif @@ -620,7 +634,7 @@ BusNetwork::BusNetwork(BusConfig &bc) void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (!_valid || pix >= _len) return; if (_rgbw) c = autoWhiteCalc(c); - if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT uint16_t offset = pix * _UDPchannels; _data[offset] = R(c); _data[offset+1] = G(c); @@ -660,25 +674,18 @@ uint32_t BusManager::memUsage(BusConfig &bc) { if (bc.type == TYPE_ONOFF || IS_PWM(bc.type)) return 5; uint16_t len = bc.count + bc.skipAmount; - uint16_t channels = 3; + uint16_t channels = Bus::getNumberOfChannels(bc.type); uint16_t multiplier = 1; if (IS_DIGITAL(bc.type)) { // digital types if (IS_16BIT(bc.type)) len *= 2; // 16-bit LEDs #ifdef ESP8266 - if (bc.type > 28) channels = 4; //RGBW if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem multiplier = 5; } #else //ESP32 RMT uses double buffer, I2S uses 5x buffer - if (bc.type > 28) channels = 4; //RGBW multiplier = 2; #endif } - if (IS_VIRTUAL(bc.type)) { - switch (bc.type) { - case TYPE_NET_DDP_RGBW: channels = 4; break; - } - } return len * channels * multiplier; //RGB } @@ -740,7 +747,7 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { if (cct >= 0) { //if white balance correction allowed, save as kelvin value instead of 0-255 if (allowWBCorrection) cct = 1900 + (cct << 5); - } else cct = -1; + } else cct = -1; // will use kelvin approximation from RGB Bus::setCCT(cct); } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 0b791adf30..c128f8c099 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -7,6 +7,9 @@ #include "const.h" +//colors.cpp +uint16_t approximateKelvinFromRGB(uint32_t rgb); + #define GET_BIT(var,bit) (((var)>>(bit))&0x01) #define SET_BIT(var,bit) ((var)|=(uint16_t)(0x0001<<(bit))) #define UNSET_BIT(var,bit) ((var)&=(~(uint16_t)(0x0001<<(bit)))) @@ -32,7 +35,7 @@ struct BusConfig { uint8_t skipAmount; bool refreshReq; uint8_t autoWhite; - uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; + uint8_t pins[5] = {255, 255, 255, 255, 255}; uint16_t frequency; bool doubleBuffer; uint8_t milliAmpsPerLed; @@ -53,9 +56,9 @@ struct BusConfig { refreshReq = (bool) GET_BIT(busType,7); type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh) size_t nPins = 1; - if (type >= TYPE_NET_DDP_RGB && type < 96) nPins = 4; //virtual network bus. 4 "pins" store IP address - else if (type > 47) nPins = 2; - else if (type > 40 && type < 46) nPins = NUM_PWM_PINS(type); + if (IS_VIRTUAL(type)) nPins = 4; //virtual network bus. 4 "pins" store IP address + else if (IS_2PIN(type)) nPins = 2; + else if (IS_PWM(type)) nPins = NUM_PWM_PINS(type); for (size_t i = 0; i < nPins; i++) pins[i] = ppins[i]; } @@ -138,6 +141,8 @@ class Bus { virtual uint16_t getLEDCurrent() { return 0; } virtual uint16_t getUsedCurrent() { return 0; } virtual uint16_t getMaxCurrent() { return 0; } + virtual uint8_t getNumberOfChannels() { return hasWhite(_type) + 3*hasRGB(_type) + hasCCT(_type); } + static inline uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } inline void setReversed(bool reversed) { _reversed = reversed; } inline uint16_t getStart() { return _start; } inline void setStart(uint16_t start) { _start = start; } @@ -154,18 +159,22 @@ class Bus { } virtual bool hasWhite(void) { return Bus::hasWhite(_type); } static bool hasWhite(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904) return true; // digital types with white channel + if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || + type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || + type == TYPE_FW1906 || type == TYPE_WS2805) return true; // digital types with white channel if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel - if (type == TYPE_NET_DDP_RGBW) return true; // network types with white channel + if (type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW) return true; // network types with white channel return false; } virtual bool hasCCT(void) { return Bus::hasCCT(_type); } static bool hasCCT(uint8_t type) { if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || - type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH) return true; + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || + type == TYPE_FW1906 || type == TYPE_WS2805 ) return true; return false; } - static void setCCT(uint16_t cct) { + static int16_t getCCT() { return _cct; } + static void setCCT(int16_t cct) { _cct = cct; } static void setCCTBlend(uint8_t b) { @@ -176,6 +185,26 @@ class Bus { if (_cctBlend > WLED_MAX_CCT_BLEND) _cctBlend = WLED_MAX_CCT_BLEND; #endif } + static void calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { + uint8_t cct = 0; //0 - full warm white, 255 - full cold white + uint8_t w = byte(c >> 24); + + if (_cct > -1) { + if (_cct >= 1900) cct = (_cct - 1900) >> 5; + else if (_cct < 256) cct = _cct; + } else { + cct = (approximateKelvinFromRGB(c) - 1900) >> 5; + } + + //0 - linear (CCT 127 = 50% warm, 50% cold), 127 - additive CCT blending (CCT 127 = 100% warm, 100% cold) + if (cct < _cctBlend) ww = 255; + else ww = ((255-cct) * 255) / (255 - _cctBlend); + if ((255-cct) < _cctBlend) cw = 255; + else cw = (cct * 255) / (255 - _cctBlend); + + ww = (w * ww) / 255; //brightness scaling + cw = (w * cw) / 255; + } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } @@ -191,8 +220,17 @@ class Bus { bool _needsRefresh; uint8_t _autoWhiteMode; uint8_t *_data; + // global Auto White Calculation override static uint8_t _gAWM; + // _cct has the following menaings (see calculateCCT() & BusManager::setSegmentCCT()): + // -1 means to extract approximate CCT value in K from RGB (in calcualteCCT()) + // [0,255] is the exact CCT value where 0 means warm and 255 cold + // [1900,10060] only for color correction expressed in K (colorBalanceFromKelvin()) static int16_t _cct; + // _cctBlend determines WW/CW blending: + // 0 - linear (CCT 127 => 50% warm, 50% cold) + // 63 - semi additive/nonlinear (CCT 127 => 66% warm, 66% cold) + // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static uint8_t _cctBlend; uint32_t autoWhiteCalc(uint32_t c); @@ -334,9 +372,12 @@ class BusManager { static void setStatusPixel(uint32_t c); static void setPixelColor(uint16_t pix, uint32_t c); static void setBrightness(uint8_t b); + // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K + // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); static void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} static uint32_t getPixelColor(uint16_t pix); + static inline int16_t getSegmentCCT() { return Bus::getCCT(); } static Bus* getBus(uint8_t busNr); diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index c63e055a87..efaad7c42f 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -2,6 +2,7 @@ #define BusWrapper_h #include "NeoPixelBusLg.h" +#include "bus_manager.h" // temporary - these defines should actually be set in platformio.ini // C3: I2S0 and I2S1 methods not supported (has one I2S bus) @@ -63,52 +64,64 @@ #define I_8266_U1_UCS_4 54 #define I_8266_DM_UCS_4 55 #define I_8266_BB_UCS_4 56 +//FW1906 GRBCW +#define I_8266_U0_FW6_5 66 +#define I_8266_U1_FW6_5 67 +#define I_8266_DM_FW6_5 68 +#define I_8266_BB_FW6_5 69 //ESP8266 APA106 #define I_8266_U0_APA106_3 81 #define I_8266_U1_APA106_3 82 #define I_8266_DM_APA106_3 83 #define I_8266_BB_APA106_3 84 +//WS2805 +#define I_8266_U0_2805_5 89 +#define I_8266_U1_2805_5 90 +#define I_8266_DM_2805_5 91 +#define I_8266_BB_2805_5 92 /*** ESP32 Neopixel methods ***/ //RGB #define I_32_RN_NEO_3 21 #define I_32_I0_NEO_3 22 #define I_32_I1_NEO_3 23 -#define I_32_BB_NEO_3 24 // bitbanging on ESP32 not recommended //RGBW #define I_32_RN_NEO_4 25 #define I_32_I0_NEO_4 26 #define I_32_I1_NEO_4 27 -#define I_32_BB_NEO_4 28 // bitbanging on ESP32 not recommended //400Kbps #define I_32_RN_400_3 29 #define I_32_I0_400_3 30 #define I_32_I1_400_3 31 -#define I_32_BB_400_3 32 // bitbanging on ESP32 not recommended //TM1814 (RGBW) #define I_32_RN_TM1_4 33 #define I_32_I0_TM1_4 34 #define I_32_I1_TM1_4 35 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //TM1829 (RGB) #define I_32_RN_TM2_3 36 #define I_32_I0_TM2_3 37 #define I_32_I1_TM2_3 38 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8903 (RGB) #define I_32_RN_UCS_3 57 #define I_32_I0_UCS_3 58 #define I_32_I1_UCS_3 59 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8904 (RGBW) #define I_32_RN_UCS_4 60 #define I_32_I0_UCS_4 61 #define I_32_I1_UCS_4 62 -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) +//FW1906 GRBCW +#define I_32_RN_FW6_5 63 +#define I_32_I0_FW6_5 64 +#define I_32_I1_FW6_5 65 +//APA106 #define I_32_RN_APA106_3 85 #define I_32_I0_APA106_3 86 #define I_32_I1_APA106_3 87 -#define I_32_BB_APA106_3 88 // bitbangging on ESP32 not recommended +//WS2805 +#define I_32_RN_2805_5 93 +#define I_32_I0_2805_5 94 +#define I_32_I1_2805_5 95 + //APA102 #define I_HS_DOT_3 39 //hardware SPI @@ -176,6 +189,16 @@ #define B_8266_U1_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio2 #define B_8266_DM_APA106_3 NeoPixelBusLg //3 chan, esp8266, gpio3 #define B_8266_BB_APA106_3 NeoPixelBusLg //3 chan, esp8266, bb (any pin but 16) +//FW1906 GRBCW +#define B_8266_U0_FW6_5 NeoPixelBusLg //esp8266, gpio1 +#define B_8266_U1_FW6_5 NeoPixelBusLg //esp8266, gpio2 +#define B_8266_DM_FW6_5 NeoPixelBusLg //esp8266, gpio3 +#define B_8266_BB_FW6_5 NeoPixelBusLg //esp8266, bb +//WS2805 GRBCW +#define B_8266_U0_2805_5 NeoPixelBusLg //esp8266, gpio1 +#define B_8266_U1_2805_5 NeoPixelBusLg //esp8266, gpio2 +#define B_8266_DM_2805_5 NeoPixelBusLg //esp8266, gpio3 +#define B_8266_BB_2805_5 NeoPixelBusLg //esp8266, bb #endif /*** ESP32 Neopixel methods ***/ @@ -183,75 +206,102 @@ //RGB #define B_32_RN_NEO_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS -#define B_32_I0_NEO_3 NeoPixelBusLg +#define B_32_I0_NEO_3 NeoPixelBusLg +//#define B_32_I0_NEO_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS -#define B_32_I1_NEO_3 NeoPixelBusLg +#define B_32_I1_NEO_3 NeoPixelBusLg +//#define B_32_I1_NEO_3 NeoPixelBusLg // parallel I2S #endif -//#define B_32_BB_NEO_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod //RGBW -#define B_32_RN_NEO_4 NeoPixelBusLg +#define B_32_RN_NEO_4 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS -#define B_32_I0_NEO_4 NeoPixelBusLg +#define B_32_I0_NEO_4 NeoPixelBusLg +//#define B_32_I0_NEO_4 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS -#define B_32_I1_NEO_4 NeoPixelBusLg +#define B_32_I1_NEO_4 NeoPixelBusLg +//#define B_32_I1_NEO_4 NeoPixelBusLg // parallel I2S #endif -//#define B_32_BB_NEO_4 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod //400Kbps #define B_32_RN_400_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_400_3 NeoPixelBusLg +//#define B_32_I0_400_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_400_3 NeoPixelBusLg +//#define B_32_I1_400_3 NeoPixelBusLg // parallel I2S #endif -//#define B_32_BB_400_3 NeoPixelBusLg // NeoEsp8266BitBang400KbpsMethod //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM1_4 NeoPixelBusLg +//#define B_32_I0_TM1_4 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_TM1_4 NeoPixelBusLg +//#define B_32_I1_TM1_4 NeoPixelBusLg // parallel I2S #endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM2_3 NeoPixelBusLg +//#define B_32_I0_TM2_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_TM2_3 NeoPixelBusLg +//#define B_32_I1_TM2_3 NeoPixelBusLg // parallel I2S #endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_3 NeoPixelBusLg +//#define B_32_I0_UCS_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_UCS_3 NeoPixelBusLg +//#define B_32_I1_UCS_3 NeoPixelBusLg // parallel I2S #endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_4 NeoPixelBusLg +//#define B_32_I0_UCS_4 NeoPixelBusLg// parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_UCS_4 NeoPixelBusLg +//#define B_32_I1_UCS_4 NeoPixelBusLg// parallel I2S #endif -//Bit Bang theoratically possible, but very undesirable and not needed (no pin restrictions on RMT and I2S) #define B_32_RN_APA106_3 NeoPixelBusLg #ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_APA106_3 NeoPixelBusLg +//#define B_32_I0_APA106_3 NeoPixelBusLg // parallel I2S #endif #ifndef WLED_NO_I2S1_PIXELBUS #define B_32_I1_APA106_3 NeoPixelBusLg +//#define B_32_I1_APA106_3 NeoPixelBusLg // parallel I2S +#endif +//FW1906 GRBCW +#define B_32_RN_FW6_5 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_FW6_5 NeoPixelBusLg +//#define B_32_I0_FW6_5 NeoPixelBusLg // parallel I2S +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_FW6_5 NeoPixelBusLg +//#define B_32_I1_FW6_5 NeoPixelBusLg // parallel I2S +#endif +//WS2805 RGBWC +#define B_32_RN_2805_5 NeoPixelBusLg +#ifndef WLED_NO_I2S0_PIXELBUS +#define B_32_I0_2805_5 NeoPixelBusLg +//#define B_32_I0_2805_5 NeoPixelBusLg // parallel I2S +#endif +#ifndef WLED_NO_I2S1_PIXELBUS +#define B_32_I1_2805_5 NeoPixelBusLg +//#define B_32_I1_2805_5 NeoPixelBusLg // parallel I2S #endif -//#define B_32_BB_APA106_3 NeoPixelBusLg // NeoEsp8266BitBang800KbpsMethod - #endif //APA102 @@ -290,6 +340,7 @@ //handles pointer type conversion for all possible bus types class PolyBus { public: + // initialize SPI bus speed for DotStar methods template static void beginDotStar(void* busPtr, int8_t sck, int8_t miso, int8_t mosi, int8_t ss, uint16_t clock_kHz = 0U) { @@ -353,6 +404,14 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Begin(); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->Begin(); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; @@ -362,7 +421,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; @@ -370,7 +428,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->Begin(); break; case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; @@ -378,7 +435,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1_4: beginTM1814(busPtr); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -396,7 +452,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; @@ -404,7 +459,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: (static_cast(busPtr))->Begin(); break; + #endif case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; @@ -412,7 +473,13 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: (static_cast(busPtr))->Begin(); break; #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: (static_cast(busPtr))->Begin(); break; + #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -429,6 +496,11 @@ class PolyBus { } static void* create(uint8_t busType, uint8_t* pins, uint16_t len, uint8_t channel, uint16_t clock_kHz = 0U) { + #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation + // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation + if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 + #endif void* busPtr = nullptr; switch (busType) { case I_NONE: break; @@ -465,6 +537,14 @@ class PolyBus { case I_8266_U1_APA106_3: busPtr = new B_8266_U1_APA106_3(len, pins[0]); break; case I_8266_DM_APA106_3: busPtr = new B_8266_DM_APA106_3(len, pins[0]); break; case I_8266_BB_APA106_3: busPtr = new B_8266_BB_APA106_3(len, pins[0]); break; + case I_8266_U0_FW6_5: busPtr = new B_8266_U0_FW6_5(len, pins[0]); break; + case I_8266_U1_FW6_5: busPtr = new B_8266_U1_FW6_5(len, pins[0]); break; + case I_8266_DM_FW6_5: busPtr = new B_8266_DM_FW6_5(len, pins[0]); break; + case I_8266_BB_FW6_5: busPtr = new B_8266_BB_FW6_5(len, pins[0]); break; + case I_8266_U0_2805_5: busPtr = new B_8266_U0_2805_5(len, pins[0]); break; + case I_8266_U1_2805_5: busPtr = new B_8266_U1_2805_5(len, pins[0]); break; + case I_8266_DM_2805_5: busPtr = new B_8266_DM_2805_5(len, pins[0]); break; + case I_8266_BB_2805_5: busPtr = new B_8266_BB_2805_5(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; @@ -474,7 +554,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); break; #endif -// case I_32_BB_NEO_3: busPtr = new B_32_BB_NEO_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; @@ -482,7 +561,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); break; #endif -// case I_32_BB_NEO_4: busPtr = new B_32_BB_NEO_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; @@ -490,7 +568,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: busPtr = new B_32_I1_400_3(len, pins[0]); break; #endif -// case I_32_BB_400_3: busPtr = new B_32_BB_400_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -508,7 +585,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: busPtr = new B_32_I1_UCS_3(len, pins[0]); break; #endif -// case I_32_BB_UCS_3: busPtr = new B_32_BB_UCS_3(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; @@ -516,7 +592,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: busPtr = new B_32_I1_UCS_4(len, pins[0]); break; #endif -// case I_32_BB_UCS_4: busPtr = new B_32_BB_UCS_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; @@ -524,7 +599,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: busPtr = new B_32_I1_APA106_3(len, pins[0]); break; #endif -// case I_32_BB_APA106_3: busPtr = new B_32_BB_APA106_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: busPtr = new B_32_I1_FW6_5(len, pins[0]); break; + #endif + case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: busPtr = new B_32_I1_2805_5(len, pins[0]); break; + #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; @@ -578,6 +666,14 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; @@ -587,7 +683,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; @@ -595,7 +690,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; @@ -603,7 +697,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -621,7 +714,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; @@ -629,7 +721,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; @@ -637,7 +728,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: (static_cast(busPtr))->Show(consistent); break; #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: (static_cast(busPtr))->Show(consistent); break; + #endif + case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: (static_cast(busPtr))->Show(consistent); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -687,6 +791,14 @@ class PolyBus { case I_8266_U1_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_APA106_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_FW6_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_2805_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_2805_5: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; @@ -696,7 +808,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_NEO_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; @@ -704,7 +815,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_NEO_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; @@ -712,7 +822,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_400_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -730,7 +839,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_UCS_3: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; @@ -738,7 +846,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_UCS_4: return (static_cast(busPtr))->CanShow(); break; case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; @@ -746,7 +853,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: return (static_cast(busPtr))->CanShow(); break; #endif -// case I_32_BB_APA106_3: return (static_cast(busPtr))->CanShow(); break; + case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: return (static_cast(busPtr))->CanShow(); break; + #endif + case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: return (static_cast(busPtr))->CanShow(); break; + #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; case I_SS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -762,12 +882,13 @@ class PolyBus { return true; } - static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co) { + static void setPixelColor(void* busPtr, uint8_t busType, uint16_t pix, uint32_t c, uint8_t co, uint16_t wwcw = 0) { uint8_t r = c >> 16; uint8_t g = c >> 8; uint8_t b = c >> 0; uint8_t w = c >> 24; RgbwColor col; + uint8_t cctWW = wwcw & 0xFF, cctCW = (wwcw>>8) & 0xFF; // reorder channels to selected order switch (co & 0x0F) { @@ -821,6 +942,14 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -830,7 +959,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; @@ -838,7 +966,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -846,7 +973,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(colB)); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -864,7 +990,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; @@ -872,7 +997,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -880,7 +1004,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #endif + case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -931,6 +1068,14 @@ class PolyBus { case I_8266_U1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_2805_5: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -940,7 +1085,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; @@ -948,7 +1092,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -956,7 +1099,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_400_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -974,7 +1116,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; @@ -982,7 +1123,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -990,7 +1130,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; #endif -// case I_32_BB_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + #endif + case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1042,6 +1195,14 @@ class PolyBus { case I_8266_U1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1051,7 +1212,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif -// case I_32_BB_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1059,7 +1219,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif -// case I_32_BB_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1067,7 +1226,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif -// case I_32_BB_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -1085,7 +1243,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; #endif -// case I_32_BB_UCS_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; @@ -1093,7 +1250,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; #endif -// case I_32_BB_UCS_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1101,7 +1257,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif -// case I_32_BB_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #endif + case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1171,6 +1340,14 @@ class PolyBus { case I_8266_U1_APA106_3: delete (static_cast(busPtr)); break; case I_8266_DM_APA106_3: delete (static_cast(busPtr)); break; case I_8266_BB_APA106_3: delete (static_cast(busPtr)); break; + case I_8266_U0_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_U1_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_DM_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_BB_FW6_5: delete (static_cast(busPtr)); break; + case I_8266_U0_2805_5: delete (static_cast(busPtr)); break; + case I_8266_U1_2805_5: delete (static_cast(busPtr)); break; + case I_8266_DM_2805_5: delete (static_cast(busPtr)); break; + case I_8266_BB_2805_5: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; @@ -1180,7 +1357,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_NEO_3: delete (static_cast(busPtr)); break; case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; @@ -1188,7 +1364,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_4: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_NEO_4: delete (static_cast(busPtr)); break; case I_32_RN_400_3: delete (static_cast(busPtr)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_400_3: delete (static_cast(busPtr)); break; @@ -1196,7 +1371,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_400_3: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_400_3: delete (static_cast(busPtr)); break; case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; #ifndef WLED_NO_I2S0_PIXELBUS @@ -1214,7 +1388,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_3: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_UCS_3: delete (static_cast(busPtr)); break; case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; @@ -1222,7 +1395,6 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_UCS_4: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_UCS_4: delete (static_cast(busPtr)); break; case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; @@ -1230,7 +1402,20 @@ class PolyBus { #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_APA106_3: delete (static_cast(busPtr)); break; #endif -// case I_32_BB_APA106_3: delete (static_cast(busPtr)); break; + case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_FW6_5: delete (static_cast(busPtr)); break; + #endif + case I_32_RN_2805_5: delete (static_cast(busPtr)); break; + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_2805_5: delete (static_cast(busPtr)); break; + #endif + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_2805_5: delete (static_cast(busPtr)); break; + #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; @@ -1292,13 +1477,17 @@ class PolyBus { return I_8266_U0_UCS_4 + offset; case TYPE_APA106: return I_8266_U0_APA106_3 + offset; + case TYPE_FW1906: + return I_8266_U0_FW6_5 + offset; + case TYPE_WS2805: + return I_8266_U0_2805_5 + offset; } #else //ESP32 - uint8_t offset = 0; //0 = RMT (num 0-7) 8 = I2S0 9 = I2S1 + uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 #if defined(CONFIG_IDF_TARGET_ESP32S2) // ESP32-S2 only has 4 RMT channels if (num > 4) return I_NONE; - if (num > 3) offset = 1; // only one I2S + if (num > 3) offset = 1; // only one I2S (use last to allow Audioreactive) #elif defined(CONFIG_IDF_TARGET_ESP32C3) // On ESP32-C3 only the first 2 RMT channels are usable for transmitting if (num > 1) return I_NONE; @@ -1310,7 +1499,8 @@ class PolyBus { #else // standard ESP32 has 8 RMT and 2 I2S channels if (num > 9) return I_NONE; - if (num > 7) offset = num -7; + if (num > 8) offset = 1; + if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) #endif switch (busType) { case TYPE_WS2812_1CH_X3: @@ -1332,11 +1522,14 @@ class PolyBus { return I_32_RN_UCS_4 + offset; case TYPE_APA106: return I_32_RN_APA106_3 + offset; + case TYPE_FW1906: + return I_32_RN_FW6_5 + offset; + case TYPE_WS2805: + return I_32_RN_2805_5 + offset; } #endif } return I_NONE; } }; - -#endif \ No newline at end of file +#endif diff --git a/wled00/button.cpp b/wled00/button.cpp index 29cb0abebf..6d69f15f80 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -7,11 +7,13 @@ #define WLED_DEBOUNCE_THRESHOLD 50 // only consider button input of at least 50ms as valid (debouncing) #define WLED_LONG_PRESS 600 // long press if button is released after held for at least 600ms #define WLED_DOUBLE_PRESS 350 // double press if another press within 350ms after a short press -#define WLED_LONG_REPEATED_ACTION 300 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 +#define WLED_LONG_REPEATED_ACTION 400 // how often a repeated action (e.g. dimming) is fired on long press on button IDs >0 #define WLED_LONG_AP 5000 // how long button 0 needs to be held to activate WLED-AP #define WLED_LONG_FACTORY_RESET 10000 // how long button 0 needs to be held to trigger a factory reset +#define WLED_LONG_BRI_STEPS 16 // how much to increase/decrease the brightness with each long press repetition static const char _mqtt_topic_button[] PROGMEM = "%s/button/%d"; // optimize flash usage +static bool buttonBriDirection = false; // true: increase brightness, false: decrease brightness void shortPressAction(uint8_t b) { @@ -21,7 +23,6 @@ void shortPressAction(uint8_t b) case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); } @@ -40,10 +41,21 @@ void longPressAction(uint8_t b) if (!macroLongPress[b]) { switch (b) { case 0: setRandomColor(col); colorUpdated(CALL_MODE_BUTTON); break; - case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action + case 1: + if(buttonBriDirection) { + if (bri == 255) break; // avoid unnecessary updates to brightness + if (bri >= 255 - WLED_LONG_BRI_STEPS) bri = 255; + else bri += WLED_LONG_BRI_STEPS; + } else { + if (bri == 1) break; // avoid unnecessary updates to brightness + if (bri <= WLED_LONG_BRI_STEPS) bri = 1; + else bri -= WLED_LONG_BRI_STEPS; + } + stateUpdated(CALL_MODE_BUTTON); + buttonPressedTime[b] = millis(); + break; // repeatable action } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); } @@ -65,7 +77,6 @@ void doublePressAction(uint8_t b) case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); } @@ -99,9 +110,13 @@ bool isButtonPressed(uint8_t i) case BTN_TYPE_TOUCH: case BTN_TYPE_TOUCH_SWITCH: #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + #ifdef SOC_TOUCH_VERSION_2 //ESP32 S2 and S3 provide a function to check touch state (state is updated in interrupt) + if (touchInterruptGetLastStatus(pin)) return true; + #else + if (digitalPinToTouchChannel(btnPin[i]) >= 0 && touchRead(pin) <= touchThreshold) return true; + #endif #endif - break; + break; } return false; } @@ -283,10 +298,12 @@ void handleButton() buttonPressedBefore[b] = true; if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press - if (!buttonLongPressed[b]) longPressAction(b); - else if (b) { //repeatable action (~3 times per s) on button > 0 + if (!buttonLongPressed[b]) { + buttonBriDirection = !buttonBriDirection; //toggle brightness direction on long press + longPressAction(b); + } else if (b) { //repeatable action (~5 times per s) on button > 0 longPressAction(b); - buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //333ms + buttonPressedTime[b] = now - WLED_LONG_REPEATED_ACTION; //200ms } buttonLongPressed[b] = true; } @@ -378,7 +395,7 @@ void handleIO() esp32RMTInvertIdle(); #endif if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, rlyMde); } offMode = false; @@ -399,10 +416,15 @@ void handleIO() esp32RMTInvertIdle(); #endif if (rlyPin>=0) { - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, !rlyMde); } } offMode = true; } } + +void IRAM_ATTR touchButtonISR() +{ + // used for ESP32 S2 and S3: nothing to do, ISR is just used to update registers of HAL driver +} diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index e51b666e48..22bfe577a3 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -110,6 +110,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); CJSON(correctWB, hw_led["cct"]); CJSON(cctFromRgb, hw_led[F("cr")]); + CJSON(cctICused, hw_led[F("ic")]); CJSON(strip.cctBlending, hw_led[F("cb")]); Bus::setCCTBlend(strip.cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS @@ -185,7 +186,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t maPerLed = elm[F("ledma")] | 55; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) - if ((ledType > TYPE_TM1814 && ledType < TYPE_WS2801) || ledType >= TYPE_NET_DDP_RGB) { // analog and virtual + if (IS_PWM(ledType) || IS_ONOFF(ledType) || IS_VIRTUAL(ledType)) { // analog and virtual maPerLed = 0; maMax = 0; } @@ -228,6 +229,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read multiple button configuration JsonObject btn_obj = hw["btn"]; + CJSON(touchThreshold, btn_obj[F("tt")]); bool pull = btn_obj[F("pull")] | (!disablePullUp); // if true, pullup is enabled disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; @@ -252,6 +254,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { btnPin[s] = -1; pinManager.deallocatePin(pin,PinOwner::Button); } + //if touch pin, enable the touch interrupt on ESP32 S2 & S3 + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so + if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) + { + touchAttachInterrupt(btnPin[s], touchButtonISR, 256 + (touchThreshold << 4)); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + } + #endif else #endif { @@ -308,7 +317,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } } } - CJSON(touchThreshold,btn_obj[F("tt")]); + CJSON(buttonPublishMqtt,btn_obj["mqtt"]); #ifndef WLED_DISABLE_INFRARED @@ -326,12 +335,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(irApplyToAllSelected, hw["ir"]["sel"]); JsonObject relay = hw[F("relay")]; + + rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { pinManager.deallocatePin(rlyPin, PinOwner::Relay); if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; - pinMode(rlyPin, OUTPUT); + pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { rlyPin = -1; } @@ -632,12 +643,12 @@ static const char s_cfg_json[] PROGMEM = "/cfg.json"; void deserializeConfigFromFS() { bool success = deserializeConfigSec(); + #ifdef WLED_ADD_EEPROM_SUPPORT if (!success) { //if file does not exist, try reading from EEPROM - #ifdef WLED_ADD_EEPROM_SUPPORT deEEPSettings(); return; - #endif } + #endif if (!requestJSONBufferLock(1)) return; @@ -767,6 +778,7 @@ void serializeConfig() { hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = correctWB; hw_led[F("cr")] = cctFromRgb; + hw_led[F("ic")] = cctICused; hw_led[F("cb")] = strip.cctBlending; hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override @@ -858,6 +870,7 @@ void serializeConfig() { JsonObject hw_relay = hw.createNestedObject(F("relay")); hw_relay["pin"] = rlyPin; hw_relay["rev"] = !rlyMde; + hw_relay[F("odrain")] = rlyOpenDrain; hw[F("baud")] = serialBaud; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 3ed54d9594..30d2c069ec 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -65,24 +65,30 @@ uint32_t color_add(uint32_t c1, uint32_t c2, bool fast) * fades color toward black * if using "video" method the resulting color will never become black unless it is already black */ + uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) { - uint8_t r = R(c1); - uint8_t g = G(c1); - uint8_t b = B(c1); - uint8_t w = W(c1); - if (video) { - r = scale8_video(r, amount); - g = scale8_video(g, amount); - b = scale8_video(b, amount); - w = scale8_video(w, amount); - } else { - r = scale8(r, amount); - g = scale8(g, amount); - b = scale8(b, amount); - w = scale8(w, amount); + uint32_t scaledcolor; // color order is: W R G B from MSB to LSB + uint32_t r = R(c1); + uint32_t g = G(c1); + uint32_t b = B(c1); + uint32_t w = W(c1); + if (video) { + uint32_t scale = amount; // 32bit for faster calculation + scaledcolor = (((r * scale) >> 8) << 16) + ((r && scale) ? 1 : 0); + scaledcolor |= (((g * scale) >> 8) << 8) + ((g && scale) ? 1 : 0); + scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); + scaledcolor |= (((w * scale) >> 8) << 24) + ((w && scale) ? 1 : 0); + return scaledcolor; + } + else { + uint32_t scale = 1 + amount; + scaledcolor = ((r * scale) >> 8) << 16; + scaledcolor |= ((g * scale) >> 8) << 8; + scaledcolor |= (b * scale) >> 8; + scaledcolor |= ((w * scale) >> 8) << 24; + return scaledcolor; } - return RGBW32(r, g, b, w); } void setRandomColor(byte* rgb) diff --git a/wled00/const.h b/wled00/const.h index dd965bc40c..0ce7b27d5c 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -53,24 +53,16 @@ #define WLED_MAX_BUSSES 3 // will allow 2 digital & 1 analog (or the other way around) #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog - #define WLED_MIN_VIRTUAL_BUSSES 4 - #else - #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog - #define WLED_MIN_VIRTUAL_BUSSES 3 - #endif + // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) + #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog + #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog #define WLED_MIN_VIRTUAL_BUSSES 4 #else - #if defined(USERMOD_AUDIOREACTIVE) // requested by @softhack007 https://github.com/blazoncek/WLED/issues/33 - #define WLED_MAX_BUSSES 8 - #define WLED_MIN_VIRTUAL_BUSSES 2 - #else - #define WLED_MAX_BUSSES 10 - #define WLED_MIN_VIRTUAL_BUSSES 0 - #endif + // the 10th digital bus (I2S0) will prevent Audioreactive usermod from functioning (it is last used though) + #define WLED_MAX_BUSSES 10 + #define WLED_MIN_VIRTUAL_BUSSES 0 #endif #endif #else @@ -180,6 +172,8 @@ #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" #define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h" +#define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h" +#define USERMOD_ID_MAX17048 48 //Usermod "usermod_max17048.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -269,9 +263,11 @@ #define TYPE_TM1829 25 #define TYPE_UCS8903 26 #define TYPE_APA106 27 +#define TYPE_FW1906 28 //RGB + CW + WW + unused channel (6 channels per IC) #define TYPE_UCS8904 29 //first RGBW digital type (hardcoded in busmanager.cpp, memUsage()) #define TYPE_SK6812_RGBW 30 #define TYPE_TM1814 31 +#define TYPE_WS2805 32 //RGB + WW + CW //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) #define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel @@ -296,6 +292,7 @@ #define IS_DIGITAL(t) (((t) > 15 && (t) < 40) || ((t) > 47 && (t) < 64)) //digital are 16-39 and 48-63 #define IS_2PIN(t) ((t) > 47 && (t) < 64) #define IS_16BIT(t) ((t) == TYPE_UCS8903 || (t) == TYPE_UCS8904) +#define IS_ONOFF(t) ((t) == 40) #define IS_PWM(t) ((t) > 40 && (t) < 46) //does not include on/Off type #define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only #define IS_VIRTUAL(t) ((t) >= 80 && (t) < 96) //this was a poor choice a better would be 96-111 @@ -373,6 +370,7 @@ //Playlist option byte #define PL_OPTION_SHUFFLE 0x01 +#define PL_OPTION_RESTORE 0x02 // Segment capability byte #define SEG_CAPABILITY_RGB 0x01 @@ -510,10 +508,10 @@ //this is merely a default now and can be changed at runtime #ifndef LEDPIN -#if defined(ESP8266) || (defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_PSRAM)) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ARDUINO_ESP32_PICO) - #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, and on boards where GPIO16 is not available +#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3) //|| (defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM)) || defined(ARDUINO_ESP32_PICO) + #define LEDPIN 2 // GPIO2 (D4) on Wemos D1 mini compatible boards, safe to use on any board #else - #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards + #define LEDPIN 16 // aligns with GPIO2 (D4) on Wemos D1 mini32 compatible boards (if it is unusable it will be reassigned in WS2812FX::finalizeInit()) #endif #endif diff --git a/wled00/data/index.css b/wled00/data/index.css index fa6e20077e..6ac3f3a1f7 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -358,7 +358,7 @@ button { #putil, #segutil, #segutil2 { min-height: 42px; - margin: 13px auto 0; + margin: 0 auto; } #segutil .segin { diff --git a/wled00/data/index.js b/wled00/data/index.js index 36c3eb1b98..bbf6bd109c 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -430,7 +430,7 @@ function presetError(empty) if (bckstr.length > 10) hasBackup = true; } catch (e) {} - var cn = `
`; + var cn = `
`; if (empty) cn += `You have no presets yet!`; else @@ -442,8 +442,8 @@ function presetError(empty) cn += `However, there is backup preset data of a previous installation available.
(Saving a preset will hide this and overwrite the backup)`; else cn += `Here is a backup of the last known good state:`; - cn += `
`; - cn += `
`; + cn += `
`; + cn += `
`; } cn += `
`; gId('pcont').innerHTML = cn; @@ -694,8 +694,6 @@ function parseInfo(i) { function populateInfo(i) { var cn=""; - var heap = i.freeheap/1024; - heap = heap.toFixed(1); var pwr = i.leds.pwr; var pwru = "Not calculated"; if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} @@ -720,11 +718,13 @@ ${inforow("Build",i.vid)} ${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} ${inforow("Uptime",getRuntimeStr(i.uptime))} ${inforow("Time",i.time)} -${inforow("Free heap",heap," kB")} +${inforow("Free heap",(i.freeheap/1024).toFixed(1)," kB")} ${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""} ${inforow("Estimated current",pwru)} ${inforow("Average FPS",i.leds.fps)} ${inforow("MAC address",i.mac)} +${inforow("CPU clock",i.clock," MHz")} +${inforow("Flash size",i.flash," MB")} ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} `; @@ -863,14 +863,11 @@ function populateSegments(s) gId("segcont").classList.remove("hide"); let noNewSegs = (lowestUnused >= maxSeg); resetUtil(noNewSegs); - if (gId('selall')) gId('selall').checked = true; for (var i = 0; i <= lSeg; i++) { if (!gId(`seg${i}`)) continue; updateLen(i); updateTrail(gId(`seg${i}bri`)); gId(`segr${i}`).classList.add("hide"); - //if (i2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");}); - var cd = gId('csl').querySelectorAll("button"); for (let e = cd.length-1; e >= 0; e--) { cd[e].dataset.r = i.col[e][0]; @@ -1838,7 +1833,7 @@ function makeSeg() }); var cn = `
`+ `
`+ - ``+ + ``+ ``+ ``+ ``+ @@ -1864,13 +1859,19 @@ function makeSeg() function resetUtil(off=false) { - gId('segutil').innerHTML = `
` + gId('segutil').innerHTML = `
` + '' + `
Add segment
` + '
' + `` + '
' + '
'; + gId('selall').checked = true; + for (var i = 0; i <= lSeg; i++) { + if (!gId(`seg${i}`)) continue; + if (!gId(`seg${i}sel`).checked) gId('selall').checked = false; // uncheck if at least one is unselected. + } + if (lSeg>2) d.querySelectorAll("#Segments .pop").forEach((e)=>{e.classList.remove("hide");}); } function makePlSel(el, incPl=false) @@ -1984,7 +1985,7 @@ function makeP(i,pl)
End preset:
diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 611653a64c..b3e04076f6 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -23,6 +23,8 @@ function isD2P(t) { return t > 47 && t < 64; } // is digital 2 pin type function is16b(t) { return t == 26 || t == 29 } // is digital 16 bit type function isVir(t) { return t >= 80 && t < 96; } // is virtual type + function hasW(t) { return (t >= 18 && t <= 21) || (t >= 28 && t <= 32) || (t >= 44 && t <= 45) || (t >= 88 && t <= 89); } + function hasCCT(t) { return t == 20 || t == 21 || t == 42 || t == 45 || t == 28 || t == 32; } // https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript function loadJS(FILE_URL, async = true) { let scE = d.createElement("script"); @@ -151,7 +153,7 @@ { const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT gId('LAdis'+n).style.display = s.selectedIndex==5 ? "inline" : "none"; - d.Sf["LA"+n].value = s.value==="0" ? 55 : s.value; + if (s.value!=="0") d.Sf["LA"+n].value = s.value; d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; } function setABL() @@ -188,6 +190,7 @@ if (isDig(t)) { if (is16b(t)) len *= 2; // 16 bit LEDs if (t > 28 && t < 40) ch = 4; //RGBW + if (t == 28) ch = 5; //GRBCW if (maxM < 10000 && d.getElementsByName("L0"+n)[0].value == 3) { //8266 DMA uses 5x the mem mul = 5; } @@ -202,7 +205,7 @@ function UI(change=false) { - let isRGBW = false, gRGBW = false, memu = 0; + let gRGBW = false, memu = 0; let busMA = 0; let sLC = 0, sPC = 0, sDI = 0, maxLC = 0; const ablEN = d.Sf.ABL.checked; @@ -242,15 +245,15 @@ d.Sf["MA"+n].min = (isVir(t) || isAna(t)) ? 0 : 250; } gId("rf"+n).onclick = (t == 31) ? (()=>{return false}) : (()=>{}); // prevent change for TM1814 - gRGBW |= isRGBW = ((t > 17 && t < 22) || (t > 28 && t < 32) || (t > 40 && t < 46 && t != 43) || t == 88); // RGBW checkbox, TYPE_xxxx values from const.h + gRGBW |= hasW(t); // RGBW checkbox, TYPE_xxxx values from const.h gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM - gId("dig"+n+"w").style.display = (isDig(t) && isRGBW) ? "inline":"none"; // show swap channels dropdown - if (!(isDig(t) && isRGBW)) d.Sf["WO"+n].value = 0; // reset swapping + gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown + if (!(isDig(t) && hasW(t))) d.Sf["WO"+n].value = 0; // reset swapping gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual gId("dig"+n+"s").style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide skip 1st for virtual & analog gId("dig"+n+"f").style.display = (isDig(t)) ? "inline":"none"; // hide refresh - gId("dig"+n+"a").style.display = (isRGBW) ? "inline":"none"; // auto calculate white + gId("dig"+n+"a").style.display = (hasW(t)) ? "inline":"none"; // auto calculate white gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off) gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed (rotated 180°)"; // change reverse text for analog //gId("psd"+n).innerHTML = isAna(t) ? "Index:":"Start:"; // change analog start description @@ -383,7 +386,9 @@ \ \ \ +\ \ +\ \ \ \ @@ -412,7 +417,7 @@
- +
PSU: mA
Color Order: @@ -614,7 +619,8 @@ } if (c.hw.relay) { d.getElementsByName("RL")[0].value = c.hw.relay.pin; - d.getElementsByName("RM")[0].checked = c.hw.relay.inv; + d.getElementsByName("RM")[0].checked = c.hw.relay.rev; + d.getElementsByName("RO")[0].checked = c.hw.relay.odrain; } UI(); } @@ -817,7 +823,7 @@

Hardware setup

Apply IR change to main segment only:
IR info
- Relay GPIO: Invert  ✕
+ Relay GPIO:  ✕ Invert Open drain

Defaults

Turn LEDs on after power up/reset:
@@ -861,6 +867,7 @@

White management


Calculate CCT from RGB:
+ CCT IC used (Athom 15W):
CCT additive blending: %

Advanced

diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 76e733671c..3c15d5a86b 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -52,40 +52,42 @@ } scanLoops = 0; - let cs = d.querySelectorAll("#wifi_entries input[type=text]"); - for (let input of (cs||[])) { - let found = false; - let select = cE("select"); - select.id = input.id; - select.name = input.name; - select.setAttribute("onchange", "T(this)"); - preScanSSID = input.value; + if (networks.length > 0) { + let cs = d.querySelectorAll("#wifi_entries input[type=text]"); + for (let input of (cs||[])) { + let found = false; + let select = cE("select"); + select.id = input.id; + select.name = input.name; + select.setAttribute("onchange", "T(this)"); + preScanSSID = input.value; - for (let i = 0; i < select.children.length; i++) { - select.removeChild(select.children[i]); - } + for (let i = 0; i < select.children.length; i++) { + select.removeChild(select.children[i]); + } - for (let i = 0; i < networks.length; i++) { - const option = cE("option"); + for (let i = 0; i < networks.length; i++) { + const option = cE("option"); + + option.setAttribute("value", networks[i].ssid); + option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; - option.setAttribute("value", networks[i].ssid); - option.textContent = `${networks[i].ssid} (${networks[i].rssi} dBm)`; + if (networks[i].ssid === input.value) { + option.setAttribute("selected", "selected"); + found = true; + } - if (networks[i].ssid === input.value) { - option.setAttribute("selected", "selected"); - found = true; + select.appendChild(option); } + const option = cE("option"); + option.setAttribute("value", "!Cs"); + option.textContent = "Other network..."; select.appendChild(option); - } - const option = cE("option"); - option.setAttribute("value", "!Cs"); - option.textContent = "Other network..."; - select.appendChild(option); - - if (input.value === "" || found) input.replaceWith(select); - else select.remove(); + if (input.value === "" || input.value === "Your_Network" || found) input.replaceWith(select); + else select.remove(); + } } button.disabled = false; diff --git a/wled00/e131.cpp b/wled00/e131.cpp index e54f60bf3c..ee8fa3949d 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -11,6 +11,7 @@ //DDP protocol support, called by handleE131Packet //handles RGB data only void handleDDPPacket(e131_packet_t* p) { + static bool ddpSeenPush = false; // have we seen a push yet? int lastPushSeq = e131LastSequenceNumber[0]; //reject late packets belonging to previous frame (assuming 4 packets max. before push) @@ -34,6 +35,7 @@ void handleDDPPacket(e131_packet_t* p) { uint16_t c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later + if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { @@ -44,7 +46,8 @@ void handleDDPPacket(e131_packet_t* p) { } bool push = p->flags & DDP_PUSH_FLAG; - if (push) { + ddpSeenPush |= push; + if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; byte sn = p->sequenceNum & 0xF; if (sn) e131LastSequenceNumber[0] = sn; @@ -184,7 +187,6 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // only apply preset if not in playlist, or playlist changed (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { presetCycCurr = dmxValPreset; - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION); } @@ -344,7 +346,6 @@ void handleArtnetPollReply(IPAddress ipAddress) { switch (DMXMode) { case DMX_MODE_DISABLED: - return; // nothing to do break; case DMX_MODE_SINGLE_RGB: @@ -389,9 +390,17 @@ void handleArtnetPollReply(IPAddress ipAddress) { break; } - for (uint16_t i = startUniverse; i <= endUniverse; ++i) { - sendArtnetPollReply(&artnetPollReply, ipAddress, i); + if (DMXMode != DMX_MODE_DISABLED) { + for (uint16_t i = startUniverse; i <= endUniverse; ++i) { + sendArtnetPollReply(&artnetPollReply, ipAddress, i); + } } + + #ifdef WLED_ENABLE_DMX + if (e131ProxyUniverse > 0 && (DMXMode == DMX_MODE_DISABLED || (e131ProxyUniverse < startUniverse || e131ProxyUniverse > endUniverse))) { + sendArtnetPollReply(&artnetPollReply, ipAddress, e131ProxyUniverse); + } + #endif } void prepareArtnetPollReply(ArtPollReply *reply) { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 7e0d6f480c..2461ebb285 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -20,6 +20,7 @@ void doublePressAction(uint8_t b=0); bool isButtonPressed(uint8_t b=0); void handleButton(); void handleIO(); +void IRAM_ATTR touchButtonISR(); //cfg.cpp bool deserializeConfig(JsonObject doc, bool fromFS = false); @@ -233,6 +234,7 @@ const char *getPresetsFileName(bool persistent = true); void initPresetsFile(); void handlePresets(); bool applyPreset(byte index, byte callMode = CALL_MODE_DIRECT_CHANGE); +bool applyPresetFromPlaylist(byte index); void applyPresetWithFallback(uint8_t presetID, uint8_t callMode, uint8_t effectID = 0, uint8_t paletteID = 0); inline bool applyTemporaryPreset() {return applyPreset(255);}; void savePreset(byte index, const char* pname = nullptr, JsonObject saveobj = JsonObject()); @@ -434,7 +436,6 @@ void handleSerial(); void updateBaudRate(uint32_t rate); //wled_server.cpp -String getFileContentType(String &filename); void createEditHandler(bool enable); void initServer(); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); diff --git a/wled00/file.cpp b/wled00/file.cpp index 37f794424d..814aa77e6b 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -375,21 +375,21 @@ void updateFSInfo() { #endif } -#if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) + +#ifdef ARDUINO_ARCH_ESP32 // caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED // original idea by @akaricchi (https://github.com/Akaricchi) -// returns a pointer to the PSRAM buffer updates size parameter +// returns a pointer to the PSRAM buffer, updates size parameter static const uint8_t *getPresetCache(size_t &size) { - static unsigned long presetsCachedTime; - static uint8_t *presetsCached; - static size_t presetsCachedSize; + static unsigned long presetsCachedTime = 0; + static uint8_t *presetsCached = nullptr; + static size_t presetsCachedSize = 0; + static byte presetsCachedValidate = 0; - if (!psramFound()) { - size = 0; - return nullptr; - } + //if (presetsModifiedTime != presetsCachedTime) DEBUG_PRINTLN(F("getPresetCache(): presetsModifiedTime changed.")); + //if (presetsCachedValidate != cacheInvalidate) DEBUG_PRINTLN(F("getPresetCache(): cacheInvalidate changed.")); - if (presetsModifiedTime != presetsCachedTime) { + if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { free(presetsCached); presetsCached = nullptr; @@ -400,6 +400,7 @@ static const uint8_t *getPresetCache(size_t &size) { File file = WLED_FS.open(FPSTR(getPresetsFileName()), "r"); if (file) { presetsCachedTime = presetsModifiedTime; + presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; presetsCached = (uint8_t*)ps_malloc(file.size() + 1); if (presetsCached) { @@ -420,26 +421,19 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){ DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; - String contentType = getFileContentType(path); - if(request->hasArg(F("download"))) contentType = F("application/octet-stream"); - /*String pathWithGz = path + ".gz"; - if(WLED_FS.exists(pathWithGz)){ - request->send(WLED_FS, pathWithGz, contentType); - return true; - }*/ - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - if (path.endsWith(FPSTR(getPresetsFileName()))) { + #ifdef ARDUINO_ARCH_ESP32 + if (psramSafe && psramFound() && path.endsWith(FPSTR(getPresetsFileName()))) { size_t psize; const uint8_t *presets = getPresetCache(psize); if (presets) { - AsyncWebServerResponse *response = request->beginResponse_P(200, contentType, presets, psize); + AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JSON), presets, psize); request->send(response); return true; } } #endif - if(WLED_FS.exists(path)) { - request->send(WLED_FS, path, contentType); + if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) { + request->send(WLED_FS, path, String(), request->hasArg(F("download"))); return true; } return false; diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 9c2fb20e7a..1536218ffc 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -210,7 +210,7 @@ void sendImprovInfoResponse() { //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); char vString[20]; - sprintf_P(vString, PSTR("0.15.0-b1/%i"), VERSION); + sprintf_P(vString, PSTR("0.15.0-b3/%i"), VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); diff --git a/wled00/json.cpp b/wled00/json.cpp index 389dc8ae56..ae8224ad32 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -142,28 +142,42 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) { if (seg.getLightCapabilities() & 3) { // segment has RGB or White - for (size_t i = 0; i < 3; i++) - { + for (size_t i = 0; i < NUM_COLORS; i++) { + // JSON "col" array can contain the following values for each of segment's colors (primary, background, custom): + // "col":[int|string|object|array, int|string|object|array, int|string|object|array] + // int = Kelvin temperature or 0 for black + // string = hex representation of [WW]RRGGBB + // object = individual channel control {"r":0,"g":127,"b":255,"w":255}, each being optional (valid to send {}) + // array = direct channel values [r,g,b,w] (w element being optional) int rgbw[] = {0,0,0,0}; bool colValid = false; JsonArray colX = colarr[i]; if (colX.isNull()) { - byte brgbw[] = {0,0,0,0}; - const char* hexCol = colarr[i]; - if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 - int kelvin = colarr[i] | -1; - if (kelvin < 0) continue; - if (kelvin == 0) seg.setColor(i, 0); - if (kelvin > 0) colorKtoRGB(kelvin, brgbw); + JsonObject oCol = colarr[i]; + if (!oCol.isNull()) { + // we have a JSON object for color {"w":123,"r":123,...}; allows individual channel control + rgbw[0] = oCol["r"] | R(seg.colors[i]); + rgbw[1] = oCol["g"] | G(seg.colors[i]); + rgbw[2] = oCol["b"] | B(seg.colors[i]); + rgbw[3] = oCol["w"] | W(seg.colors[i]); colValid = true; - } else { //HEX string, e.g. "FFAA00" - colValid = colorFromHexString(brgbw, hexCol); + } else { + byte brgbw[] = {0,0,0,0}; + const char* hexCol = colarr[i]; + if (hexCol == nullptr) { //Kelvin color temperature (or invalid), e.g 2400 + int kelvin = colarr[i] | -1; + if (kelvin < 0) continue; + if (kelvin == 0) seg.setColor(i, 0); + if (kelvin > 0) colorKtoRGB(kelvin, brgbw); + colValid = true; + } else { //HEX string, e.g. "FFAA00" + colValid = colorFromHexString(brgbw, hexCol); + } + for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } - for (size_t c = 0; c < 4; c++) rgbw[c] = brgbw[c]; } else { //Array of ints (RGB or RGBW color), e.g. [255,160,0] byte sz = colX.size(); if (sz == 0) continue; //do nothing on empty array - copyArray(colX, rgbw, 4); colValid = true; } @@ -226,14 +240,19 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) getVal(elem["ix"], &seg.intensity); uint8_t pal = seg.palette; + last = strip.getPaletteCount(); + if (!elem["pal"].isNull() && elem["pal"].is()) { + const char *tmp = elem["pal"].as(); + if (strlen(tmp) > 3 && (strchr(tmp,'r') || strchr(tmp,'~') != strrchr(tmp,'~'))) last = 0; // we have "X~Y(r|[w]~[-])" form + } if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments - if (getVal(elem["pal"], &pal)) seg.setPalette(pal); + if (getVal(elem["pal"], &pal, 0, last)) seg.setPalette(pal); } getVal(elem["c1"], &seg.custom1); getVal(elem["c2"], &seg.custom2); uint8_t cust3 = seg.custom3; - getVal(elem["c3"], &cust3); // we can't pass reference to bitfield + getVal(elem["c3"], &cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); @@ -257,8 +276,8 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) seg.fill(BLACK); } - uint16_t start = 0, stop = 0; - byte set = 0; //0 nothing set, 1 start set, 2 range set + start = 0, stop = 0; + set = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { if(iarr[i].is()) { @@ -298,7 +317,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) return true; } -// deserializes WLED state (fileDoc points to doc object if called from web server) +// deserializes WLED state // presetId is non-0 if called from handlePreset() bool deserializeState(JsonObject root, byte callMode, byte presetId) { @@ -442,13 +461,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) currentPreset = root[F("pd")] | currentPreset; if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp] presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after - //unloadPlaylist(); // applying a preset unloads the playlist, may be needed here too? } else if (!root["ps"].isNull()) { ps = presetCycCurr; if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) presetCycCurr = ps; - unloadPlaylist(); // applying a preset unloads the playlist applyPreset(ps, callMode); // async load from file system (only preset ID was specified) return stateResponse; } @@ -619,6 +636,7 @@ void serializeInfo(JsonObject root) root[F("ver")] = versionString; root[F("vid")] = VERSION; root[F("cn")] = F(WLED_CODENAME); + root[F("release")] = FPSTR(releaseString); JsonObject leds = root.createNestedObject(F("leds")); leds[F("count")] = strip.getLengthTotal(); @@ -732,6 +750,8 @@ void serializeInfo(JsonObject root) root[F("arch")] = ESP.getChipModel(); #endif root[F("core")] = ESP.getSdkVersion(); + root[F("clock")] = ESP.getCpuFreqMHz(); + root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = ESP.getMaxAllocHeap(); root[F("resetReason0")] = (int)rtc_get_reset_reason(0); @@ -741,6 +761,8 @@ void serializeInfo(JsonObject root) #else root[F("arch")] = "esp8266"; root[F("core")] = ESP.getCoreVersion(); + root[F("clock")] = ESP.getCpuFreqMHz(); + root[F("flash")] = (ESP.getFlashChipSize()/1024)/1024; #ifdef WLED_DEBUG root[F("maxalloc")] = ESP.getMaxFreeBlockSize(); root[F("resetReason")] = (int)ESP.getResetInfoPtr()->reason; @@ -749,8 +771,8 @@ void serializeInfo(JsonObject root) #endif root[F("freeheap")] = ESP.getFreeHeap(); - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) - if (psramFound()) root[F("psram")] = ESP.getFreePsram(); + #if defined(ARDUINO_ARCH_ESP32) + if (psramSafe && psramFound()) root[F("psram")] = ESP.getFreePsram(); #endif root[F("uptime")] = millis()/1000 + rolloverMillis*4294967; @@ -852,8 +874,8 @@ void serializePalettes(JsonObject root, int page) int itemPerPage = 8; #endif - int palettesCount = strip.getPaletteCount(); int customPalettes = strip.customPalettes.size(); + int palettesCount = strip.getPaletteCount() - customPalettes; int maxPage = (palettesCount + customPalettes -1) / itemPerPage; if (page > maxPage) page = maxPage; @@ -1065,7 +1087,7 @@ void serveJson(AsyncWebServerRequest* request) } #endif else if (url.indexOf("pal") > 0) { - request->send_P(200, "application/json", JSON_palette_names); // contentType defined in AsyncJson-v6.h + request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { @@ -1152,10 +1174,10 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) } #endif - char buffer[2048]; // shoud be enough for 256 LEDs [RRGGBB] + all other text (9+25) - strcpy_P(buffer, PSTR("{\"leds\":[")); - obuf = buffer; // assign buffer for oappnd() functions - olen = 9; + DynamicBuffer buffer(9 + (9*(1+(used/n))) + 7 + 5 + 6 + 5 + 6 + 5 + 2); + char* buf = buffer.data(); // assign buffer for oappnd() functions + strncpy_P(buffer.data(), PSTR("{\"leds\":["), buffer.size()); + buf += 9; // sizeof(PSTR()) from last line for (size_t i = 0; i < used; i += n) { @@ -1170,29 +1192,27 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map g = scale8(qadd8(w, g), strip.getBrightness()); //G b = scale8(qadd8(w, b), strip.getBrightness()); //B - olen += sprintf_P(obuf + olen, PSTR("\"%06X\","), RGBW32(r,g,b,0)); + buf += sprintf_P(buf, PSTR("\"%06X\","), RGBW32(r,g,b,0)); } - olen -= 1; - oappend((const char*)F("],\"n\":")); - oappendi(n); + buf--; // remove last comma + buf += sprintf_P(buf, PSTR("],\"n\":%d"), n); #ifndef WLED_DISABLE_2D if (strip.isMatrix) { - oappend((const char*)F(",\"w\":")); - oappendi(Segment::maxWidth/n); - oappend((const char*)F(",\"h\":")); - oappendi(Segment::maxHeight/n); + buf += sprintf_P(buf, PSTR(",\"w\":%d"), Segment::maxWidth/n); + buf += sprintf_P(buf, PSTR(",\"h\":%d"), Segment::maxHeight/n); } #endif - oappend("}"); + (*buf++) = '}'; + (*buf++) = 0; + if (request) { - request->send(200, "application/json", buffer); // contentType defined in AsyncJson-v6.h + request->send(200, FPSTR(CONTENT_TYPE_JSON), toString(std::move(buffer))); } #ifdef WLED_ENABLE_WEBSOCKETS else { - wsc->text(obuf, olen); + wsc->text(toString(std::move(buffer))); } - #endif - obuf = nullptr; + #endif return true; } #endif diff --git a/wled00/led.cpp b/wled00/led.cpp index ba772df9b9..23c8d03c57 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -172,7 +172,9 @@ void updateInterfaces(uint8_t callMode) espalexaDevice->setColor(col[0], col[1], col[2]); } #endif - doPublishMqtt = true; + #ifndef WLED_DISABLE_MQTT + publishMqtt(); + #endif } @@ -180,9 +182,6 @@ void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); -#ifndef WLED_DISABLE_MQTT - if (doPublishMqtt) publishMqtt(); -#endif if (transitionActive && strip.getTransition() > 0) { float tper = (millis() - transitionStartTime)/(float)strip.getTransition(); diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 3c753a9a90..2e2e4a6bd2 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -47,8 +47,8 @@ void onMqttConnect(bool sessionPresent) usermods.onMqttConnect(sessionPresent); - doPublishMqtt = true; DEBUG_PRINTLN(F("MQTT ready")); + publishMqtt(); } @@ -76,7 +76,7 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties if (index + len >= total) { // at end payloadStr[total] = '\0'; // terminate c style string } else { - DEBUG_PRINTLN(F("Partial packet received.")); + DEBUG_PRINTLN(F("MQTT partial packet received.")); return; // process next packet } DEBUG_PRINTLN(payloadStr); @@ -131,13 +131,12 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties void publishMqtt() { - doPublishMqtt = false; if (!WLED_MQTT_CONNECTED) return; DEBUG_PRINTLN(F("Publish MQTT")); #ifndef USERMOD_SMARTNEST char s[10]; - char subuf[38]; + char subuf[48]; sprintf_P(s, PSTR("%u"), bri); strlcpy(subuf, mqttDeviceTopic, 33); diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index 0b2cf36652..d473186edf 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -399,7 +399,6 @@ void checkTimers() && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) ) { - unloadPlaylist(); applyPreset(timerMacro[i]); } } @@ -413,7 +412,6 @@ void checkTimers() && (timerWeekday[8] & 0x01) //timer is enabled && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { - unloadPlaylist(); applyPreset(timerMacro[8]); DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); } @@ -428,7 +426,6 @@ void checkTimers() && (timerWeekday[9] & 0x01) //timer is enabled && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week { - unloadPlaylist(); applyPreset(timerMacro[9]); DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); } diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index 044dc6c92e..dd00943d88 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -209,11 +209,10 @@ bool PinManagerClass::allocatePin(byte gpio, bool output, PinOwner tag) // if tag is set to PinOwner::None, checks for ANY owner of the pin. // if tag is set to any other value, checks if that tag is the current owner of the pin. -bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) +bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) const { if (!isPinOk(gpio, false)) return true; if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false; - if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access byte by = gpio >> 3; byte bi = gpio - (by<<3); return bitRead(pinAlloc[by], bi); @@ -236,8 +235,9 @@ bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) */ // Check if supplied GPIO is ok to use -bool PinManagerClass::isPinOk(byte gpio, bool output) +bool PinManagerClass::isPinOk(byte gpio, bool output) const { + if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access #ifdef ARDUINO_ARCH_ESP32 if (digitalPinIsValid(gpio)) { #if defined(CONFIG_IDF_TARGET_ESP32C3) @@ -248,7 +248,7 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) // 00 to 18 are for general use. Be careful about straping pins GPIO0 and GPIO3 - these may be pulled-up or pulled-down on your board. if (gpio > 18 && gpio < 21) return false; // 19 + 20 = USB-JTAG. Not recommended for other uses. if (gpio > 21 && gpio < 33) return false; // 22 to 32: not connected + SPI FLASH - //if (gpio > 32 && gpio < 38) return false; // 33 to 37: not available if using _octal_ SPI Flash or _octal_ PSRAM + if (gpio > 32 && gpio < 38) return !psramFound(); // 33 to 37: not available if using _octal_ SPI Flash or _octal_ PSRAM // 38 to 48 are for general use. Be careful about straping pins GPIO45 and GPIO46 - these may be pull-up or pulled-down on your board. #elif defined(CONFIG_IDF_TARGET_ESP32S2) // strapping pins: 0, 45 & 46 @@ -257,9 +257,8 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) // GPIO46 is input only and pulled down #else if (gpio > 5 && gpio < 12) return false; //SPI flash pins - #ifdef BOARD_HAS_PSRAM - if (gpio == 16 || gpio == 17) return false; //PSRAM pins - #endif + if (strncmp_P(PSTR("ESP32-PICO"), ESP.getChipModel(), 10) == 0 && (gpio == 16 || gpio == 17)) return false; // PICO-D4: gpio16+17 are in use for onboard SPI FLASH + if (gpio == 16 || gpio == 17) return !psramFound(); //PSRAM pins on ESP32 (these are IO) #endif if (output) return digitalPinCanOutput(gpio); else return true; @@ -272,8 +271,8 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) return false; } -PinOwner PinManagerClass::getPinOwner(byte gpio) { - if (gpio >= WLED_NUM_PINS) return PinOwner::None; // catch error case, to avoid array out-of-bounds access +PinOwner PinManagerClass::getPinOwner(byte gpio) const +{ if (!isPinOk(gpio, false)) return PinOwner::None; return ownerTag[gpio]; } diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 6a50df588e..398e9bdc0e 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -61,7 +61,8 @@ enum struct PinOwner : uint8_t { UM_Audioreactive = USERMOD_ID_AUDIOREACTIVE, // 0x20 // Usermod "audio_reactive.h" UM_SdCard = USERMOD_ID_SD_CARD, // 0x25 // Usermod "usermod_sd_card.h" UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" - UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" + UM_MAX17048 = USERMOD_ID_MAX17048 // 0x2F // Usermod "usermod_max17048.h" }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); @@ -108,11 +109,11 @@ class PinManagerClass { inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } // will return true for reserved pins - bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); + bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None) const; // will return false for reserved pins - bool isPinOk(byte gpio, bool output = true); + bool isPinOk(byte gpio, bool output = true) const; - PinOwner getPinOwner(byte gpio); + PinOwner getPinOwner(byte gpio) const; #ifdef ARDUINO_ARCH_ESP32 byte allocateLedc(byte channels); diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index bcbcb4516f..67c4f60494 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -109,7 +109,10 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { if (playlistRepeat > 0) playlistRepeat++; //add one extra repetition immediately since it will be deducted on first start playlistEndPreset = playlistObj["end"] | 0; // if end preset is 255 restore original preset (if any running) upon playlist end - if (playlistEndPreset == 255 && currentPreset > 0) playlistEndPreset = currentPreset; + if (playlistEndPreset == 255 && currentPreset > 0) { + playlistEndPreset = currentPreset; + playlistOptions |= PL_OPTION_RESTORE; // for async save operation + } if (playlistEndPreset > 250) playlistEndPreset = 0; shuffle = shuffle || playlistObj["r"]; if (shuffle) playlistOptions |= PL_OPTION_SHUFFLE; @@ -122,8 +125,7 @@ int16_t loadPlaylist(JsonObject playlistObj, byte presetId) { void handlePlaylist() { static unsigned long presetCycledTime = 0; - // if fileDoc is not null JSON buffer is in use so just quit - if (currentPlaylist < 0 || playlistEntries == nullptr || fileDoc != nullptr) return; + if (currentPlaylist < 0 || playlistEntries == nullptr) return; if (millis() - presetCycledTime > (100*playlistEntryDur)) { presetCycledTime = millis(); @@ -135,7 +137,7 @@ void handlePlaylist() { if (!playlistIndex) { if (playlistRepeat == 1) { //stop if all repetitions are done unloadPlaylist(); - if (playlistEndPreset) applyPreset(playlistEndPreset); + if (playlistEndPreset) applyPresetFromPlaylist(playlistEndPreset); return; } if (playlistRepeat > 1) playlistRepeat--; // decrease repeat count on each index reset if not an endless playlist @@ -146,7 +148,7 @@ void handlePlaylist() { jsonTransitionOnce = true; strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); playlistEntryDur = playlistEntries[playlistIndex].dur; - applyPreset(playlistEntries[playlistIndex].preset); + applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); } } @@ -157,7 +159,7 @@ void serializePlaylist(JsonObject sObj) { JsonArray dur = playlist.createNestedArray("dur"); JsonArray transition = playlist.createNestedArray(F("transition")); playlist[F("repeat")] = (playlistIndex < 0 && playlistRepeat > 0) ? playlistRepeat - 1 : playlistRepeat; // remove added repetition count (if not yet running) - playlist["end"] = playlistEndPreset; + playlist["end"] = playlistOptions & PL_OPTION_RESTORE ? 255 : playlistEndPreset; playlist["r"] = playlistOptions & PL_OPTION_SHUFFLE; for (int i=0; ito(); @@ -53,23 +53,21 @@ static void doSaveState() { #if defined(ARDUINO_ARCH_ESP32) if (!persist) { if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); - size_t len = measureJson(*fileDoc) + 1; + size_t len = measureJson(*pDoc) + 1; DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - if (psramFound()) + if (psramSafe && psramFound()) tmpRAMbuffer = (char*) ps_malloc(len); else - #endif tmpRAMbuffer = (char*) malloc(len); if (tmpRAMbuffer!=nullptr) { - serializeJson(*fileDoc, tmpRAMbuffer, len); + serializeJson(*pDoc, tmpRAMbuffer, len); } else { - writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); } } else #endif - writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(persist), presetToSave, pDoc); if (persist) presetsModifiedTime = toki.second(); //unix time releaseJSONBufferLock(); @@ -117,8 +115,18 @@ void initPresetsFile() f.close(); } +bool applyPresetFromPlaylist(byte index) +{ + DEBUG_PRINT(F("Request to apply preset: ")); + DEBUG_PRINTLN(index); + presetToApply = index; + callModeToApply = CALL_MODE_DIRECT_CHANGE; + return true; +} + bool applyPreset(byte index, byte callMode) { + unloadPlaylist(); // applying a preset unloads the playlist (#3827) DEBUG_PRINT(F("Request to apply preset: ")); DEBUG_PRINTLN(index); presetToApply = index; @@ -144,7 +152,7 @@ void handlePresets() return; } - if (presetToApply == 0 || fileDoc) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free + if (presetToApply == 0 || !requestJSONBufferLock(9)) return; // no preset waiting to apply, or JSON buffer is already allocated, return to loop until free bool changePreset = false; uint8_t tmpPreset = presetToApply; // store temporary since deserializeState() may call applyPreset() @@ -152,9 +160,6 @@ void handlePresets() JsonObject fdo; - // allocate buffer - if (!requestJSONBufferLock(9)) return; // will also assign fileDoc - presetToApply = 0; //clear request for preset callModeToApply = 0; @@ -163,14 +168,14 @@ void handlePresets() #ifdef ARDUINO_ARCH_ESP32 if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - deserializeJson(*fileDoc,tmpRAMbuffer); + deserializeJson(*pDoc,tmpRAMbuffer); errorFlag = ERR_NONE; } else #endif { - errorFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, fileDoc) ? ERR_NONE : ERR_FS_PLOAD; + errorFlag = readObjectFromFileUsingId(getPresetsFileName(tmpPreset < 255), tmpPreset, pDoc) ? ERR_NONE : ERR_FS_PLOAD; } - fdo = fileDoc->as(); + fdo = pDoc->as(); //HTTP API commands const char* httpwin = fdo["win"]; @@ -197,13 +202,13 @@ void handlePresets() } #endif - releaseJSONBufferLock(); // will also clear fileDoc + releaseJSONBufferLock(); if (changePreset) notify(tmpMode); // force UDP notification stateUpdated(tmpMode); // was colorUpdated() if anything breaks updateInterfaces(tmpMode); } -//called from handleSet(PS=) [network callback (fileDoc==nullptr), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] +//called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { if (!saveName) saveName = new char[33]; @@ -241,7 +246,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) if (sObj[F("playlist")].isNull()) { // we will save API call immediately (often causes presets.json corruption) presetToSave = 0; - if (index <= 250 && fileDoc) { // cannot save API calls to temporary preset (255) + if (index <= 250) { // cannot save API calls to temporary preset (255) sObj.remove("o"); sObj.remove("v"); sObj.remove("time"); @@ -249,7 +254,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) sObj.remove(F("psave")); if (sObj["n"].isNull()) sObj["n"] = saveName; initPresetsFile(); // just in case if someone deleted presets.json using /edit - writeObjectToFileUsingId(getPresetsFileName(), index, fileDoc); + writeObjectToFileUsingId(getPresetsFileName(), index, pDoc); presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 49fbc4b020..54cdf31f60 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -108,7 +108,6 @@ static void setOff() { void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { resetNightMode(); - unloadPlaylist(); applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } diff --git a/wled00/set.cpp b/wled00/set.cpp index 4e2e60b3da..d3382be187 100755 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -110,6 +110,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) for (uint8_t s=0; s=0 && pinManager.isPinAllocated(btnPin[s], PinOwner::Button)) { pinManager.deallocatePin(btnPin[s], PinOwner::Button); + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt + if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin + touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing + #endif } } @@ -123,6 +127,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) autoSegments = request->hasArg(F("MS")); correctWB = request->hasArg(F("CCT")); cctFromRgb = request->hasArg(F("CR")); + cctICused = request->hasArg(F("IC")); strip.cctBlending = request->arg(F("CB")).toInt(); Bus::setCCTBlend(strip.cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); @@ -238,8 +243,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) rlyPin = -1; } rlyMde = (bool)request->hasArg(F("RM")); + rlyOpenDrain = (bool)request->hasArg(F("RO")); disablePullUp = (bool)request->hasArg(F("IP")); + touchThreshold = request->arg(F("TT")).toInt(); for (uint8_t i=0; i10) char be[4] = "BE"; be[2] = (i<10?48:55)+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) @@ -256,12 +263,21 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) btnPin[i] = -1; pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); } - else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH) && digitalPinToTouchChannel(btnPin[i]) < 0) + else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH)) { - // not a touch pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); - btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + if (digitalPinToTouchChannel(btnPin[i]) < 0) + { + // not a touch pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); + btnPin[i] = -1; + pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + } + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so + else + { + touchAttachInterrupt(btnPin[i], touchButtonISR, 256 + (touchThreshold << 4)); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + } + #endif } else #endif @@ -281,7 +297,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) buttonType[i] = BTN_TYPE_NONE; } } - touchThreshold = request->arg(F("TT")).toInt(); briS = request->arg(F("CA")).toInt(); @@ -883,7 +898,6 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //apply preset if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { - unloadPlaylist(); applyPreset(presetCycCurr); } diff --git a/wled00/src/dependencies/json/AsyncJson-v6.h b/wled00/src/dependencies/json/AsyncJson-v6.h index 32ac546077..4a127dedbc 100644 --- a/wled00/src/dependencies/json/AsyncJson-v6.h +++ b/wled00/src/dependencies/json/AsyncJson-v6.h @@ -21,8 +21,6 @@ #define DYNAMIC_JSON_DOCUMENT_SIZE 16384 #endif -constexpr const char* JSON_MIMETYPE = "application/json"; - /* * Json Response * */ @@ -66,7 +64,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse { AsyncJsonResponse(JsonDocument *ref, bool isArray=false) : _jsonBuffer(1), _isValid{false} { _code = 200; - _contentType = JSON_MIMETYPE; + _contentType = FPSTR(CONTENT_TYPE_JSON); if(isArray) _root = ref->to(); else @@ -75,7 +73,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse { AsyncJsonResponse(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE, bool isArray=false) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { _code = 200; - _contentType = JSON_MIMETYPE; + _contentType = FPSTR(CONTENT_TYPE_JSON); if(isArray) _root = _jsonBuffer.createNestedArray(); else diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index bcfb41fa3b..3f34d18fef 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -209,6 +209,14 @@ #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" #endif +#ifdef USERMOD_MAX17048 + #include "../usermods/MAX17048_v2/usermod_max17048.h" +#endif + +#ifdef USERMOD_TETRISAI + #include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" +#endif + void registerUsermods() { /* @@ -405,4 +413,12 @@ void registerUsermods() #ifdef USERMOD_STAIRCASE_WIPE usermods.add(new StairwayWipeUsermod()); #endif + + #ifdef USERMOD_MAX17048 + usermods.add(new Usermod_MAX17048()); + #endif + + #ifdef USERMOD_TETRISAI + usermods.add(new TetrisAIUsermod()); + #endif } diff --git a/wled00/util.cpp b/wled00/util.cpp index fa6c8faffd..ad7e4b6701 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -228,7 +228,6 @@ bool requestJSONBufferLock(uint8_t module) DEBUG_PRINT(F("JSON buffer locked. (")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); - fileDoc = pDoc; // used for applying presets (presets.cpp) pDoc->clear(); return true; } @@ -239,7 +238,6 @@ void releaseJSONBufferLock() DEBUG_PRINT(F("JSON buffer released. (")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); - fileDoc = nullptr; jsonBufferLock = 0; } @@ -265,8 +263,8 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } - if (src == JSON_palette_names && mode > GRADIENT_PALETTE_COUNT) { - snprintf_P(dest, maxLen, PSTR("~ Custom %d~"), 255-mode); + if (src == JSON_palette_names && mode > (GRADIENT_PALETTE_COUNT + 13)) { + snprintf_P(dest, maxLen, PSTR("~ Custom %d ~"), 255-mode); dest[maxLen-1] = '\0'; return strlen(dest); } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index fc16dabcc3..eb78608516 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -240,10 +240,11 @@ void WLED::loop() DEBUG_PRINT(F("Runtime: ")); DEBUG_PRINTLN(millis()); DEBUG_PRINT(F("Unix time: ")); toki.printTime(toki.getTime()); DEBUG_PRINT(F("Free heap: ")); DEBUG_PRINTLN(ESP.getFreeHeap()); - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) + #if defined(ARDUINO_ARCH_ESP32) if (psramFound()) { DEBUG_PRINT(F("Total PSRAM: ")); DEBUG_PRINT(ESP.getPsramSize()/1024); DEBUG_PRINTLN("kB"); DEBUG_PRINT(F("Free PSRAM: ")); DEBUG_PRINT(ESP.getFreePsram()/1024); DEBUG_PRINTLN("kB"); + if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); } #endif DEBUG_PRINT(F("Wifi state: ")); DEBUG_PRINTLN(WiFi.status()); @@ -269,6 +270,7 @@ void WLED::loop() maxLoopMillis = 0; maxUsermodMillis = 0; maxStripMillis = 0; + avgLoopMillis = 0; avgUsermodMillis = 0; avgStripMillis = 0; debugTime = millis(); @@ -360,57 +362,27 @@ void WLED::setup() DEBUG_PRINT(F(", speed ")); DEBUG_PRINT(ESP.getFlashChipSpeed()/1000000);DEBUG_PRINTLN(F("MHz.")); #else - DEBUG_PRINT(F("esp8266 ")); + DEBUG_PRINT(F("esp8266 @ ")); DEBUG_PRINT(ESP.getCpuFreqMHz()); DEBUG_PRINT(F("MHz.\nCore: ")); DEBUG_PRINTLN(ESP.getCoreVersion()); + DEBUG_PRINT(F("FLASH: ")); DEBUG_PRINT((ESP.getFlashChipSize()/1024)/1024); DEBUG_PRINTLN(F(" MB")); #endif DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) -/* - * The following code is obsolete as PinManager::isPinOK() will return false for reserved GPIO. - * Additionally xml.cpp will inform UI about reserved GPIO. - * - - #if defined(CONFIG_IDF_TARGET_ESP32S3) - // S3: reserve GPIO 33-37 for "octal" PSRAM - managed_pin_type pins[] = { {33, true}, {34, true}, {35, true}, {36, true}, {37, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #elif defined(CONFIG_IDF_TARGET_ESP32S2) - // S2: reserve GPIO 26-32 for PSRAM (may fail due to isPinOk() but that will also prevent other allocation) - managed_pin_type pins[] = { {26, true}, {27, true}, {28, true}, {29, true}, {30, true}, {31, true}, {32, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #elif defined(CONFIG_IDF_TARGET_ESP32C3) - // C3: reserve GPIO 12-17 for PSRAM (may fail due to isPinOk() but that will also prevent other allocation) - managed_pin_type pins[] = { {12, true}, {13, true}, {14, true}, {15, true}, {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); - #else - // GPIO16/GPIO17 reserved for SPI RAM - managed_pin_type pins[] = { {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); +#if defined(ARDUINO_ARCH_ESP32) + // BOARD_HAS_PSRAM also means that a compiler flag "-mfix-esp32-psram-cache-issue" was used and so PSRAM is safe to use on rev.1 ESP32 + #if !defined(BOARD_HAS_PSRAM) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) + if (psramFound() && ESP.getChipRevision() < 3) psramSafe = false; + if (!psramSafe) DEBUG_PRINTLN(F("Not using PSRAM.")); #endif -*/ - #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) - pDoc = new PSRAMDynamicJsonDocument(2*JSON_BUFFER_SIZE); - if (!pDoc) pDoc = new PSRAMDynamicJsonDocument(JSON_BUFFER_SIZE); // falback if double sized buffer could not be allocated - // if the above still fails requestJsonBufferLock() will always return false preventing crashes + pDoc = new PSRAMDynamicJsonDocument((psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); + DEBUG_PRINT(F("JSON buffer allocated: ")); DEBUG_PRINTLN((psramSafe && psramFound() ? 2 : 1)*JSON_BUFFER_SIZE); + // if the above fails requestJsonBufferLock() will always return false preventing crashes if (psramFound()) { DEBUG_PRINT(F("Total PSRAM: ")); DEBUG_PRINT(ESP.getPsramSize()/1024); DEBUG_PRINTLN("kB"); DEBUG_PRINT(F("Free PSRAM : ")); DEBUG_PRINT(ESP.getFreePsram()/1024); DEBUG_PRINTLN("kB"); } - #else - if (!pDoc) pDoc = &gDoc; // just in case ... (it should be globally assigned) - DEBUG_PRINTLN(F("PSRAM not used.")); - #endif -#endif -#if defined(ARDUINO_ESP32_PICO) - // special handling for PICO-D4: gpio16+17 are in use for onboard SPI FLASH (not PSRAM) - managed_pin_type pins[] = { {16, true}, {17, true} }; - pinManager.allocateMultiplePins(pins, sizeof(pins)/sizeof(managed_pin_type), PinOwner::SPI_RAM); #endif - //DEBUG_PRINT(F("LEDs inited. heap usage ~")); - //DEBUG_PRINTLN(heapPreAlloc - ESP.getFreeHeap()); - #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) pinManager.allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif @@ -451,6 +423,7 @@ void WLED::setup() DEBUG_PRINTLN(F("Reading config")); deserializeConfigFromFS(); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); #if defined(STATUSLED) && STATUSLED>=0 if (!pinManager.isPinAllocated(STATUSLED)) { @@ -554,7 +527,7 @@ void WLED::setup() void WLED::beginStrip() { // Initialize NeoPixel Strip and button - strip.finalizeInit(); // busses created during deserializeConfig() + strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.makeAutoSegments(); strip.setBrightness(0); strip.setShowCallback(handleOverlayDraw); diff --git a/wled00/wled.h b/wled00/wled.h index 63ab1c2977..139c451f82 100755 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -3,12 +3,12 @@ /* Main sketch, global variable declarations @title WLED project sketch - @version 0.15.0-b1 + @version 0.15.0-b3 @author Christian Schwinne */ // version code in format yymmddb (b = daily build) -#define VERSION 2403100 +#define VERSION 2404120 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -158,15 +158,16 @@ // The following is a construct to enable code to compile without it. // There is a code that will still not use PSRAM though: // AsyncJsonResponse is a derived class that implements DynamicJsonDocument (AsyncJson-v6.h) -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) +#if defined(ARDUINO_ARCH_ESP32) +extern bool psramSafe; struct PSRAM_Allocator { void* allocate(size_t size) { - if (psramFound()) return ps_malloc(size); // use PSRAM if it exists - else return malloc(size); // fallback + if (psramSafe && psramFound()) return ps_malloc(size); // use PSRAM if it exists + else return malloc(size); // fallback } void* reallocate(void* ptr, size_t new_size) { - if (psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists - else return realloc(ptr, new_size); // fallback + if (psramSafe && psramFound()) return ps_realloc(ptr, new_size); // use PSRAM if it exists + else return realloc(ptr, new_size); // fallback } void deallocate(void* pointer) { free(pointer); @@ -242,27 +243,32 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument; // int arr[]{0,1,2} becomes WLED_GLOBAL int arr[] _INIT_N(({0,1,2})); #ifndef WLED_DEFINE_GLOBAL_VARS -# define WLED_GLOBAL extern -# define _INIT(x) -# define _INIT_N(x) + #define WLED_GLOBAL extern + #define _INIT(x) + #define _INIT_N(x) + #define _INIT_PROGMEM(x) #else -# define WLED_GLOBAL -# define _INIT(x) = x - -//needed to ignore commas in array definitions -#define UNPACK( ... ) __VA_ARGS__ -# define _INIT_N(x) UNPACK x + #define WLED_GLOBAL + #define _INIT(x) = x + //needed to ignore commas in array definitions + #define UNPACK( ... ) __VA_ARGS__ + #define _INIT_N(x) UNPACK x + #define _INIT_PROGMEM(x) PROGMEM = x #endif #define STRINGIFY(X) #X #define TOSTRING(X) STRINGIFY(X) #ifndef WLED_VERSION - #define WLED_VERSION "dev" + #define WLED_VERSION dev +#endif +#ifndef WLED_RELEASE_NAME + #define WLED_RELEASE_NAME dev_release #endif // Global Variable definitions WLED_GLOBAL char versionString[] _INIT(TOSTRING(WLED_VERSION)); +WLED_GLOBAL char releaseString[] _INIT_PROGMEM(TOSTRING(WLED_RELEASE_NAME)); // somehow this will not work if using "const char releaseString[] #define WLED_CODENAME "Kōsen" // AP and OTA default passwords (for maximum security change them!) @@ -287,6 +293,12 @@ WLED_GLOBAL bool rlyMde _INIT(true); #else WLED_GLOBAL bool rlyMde _INIT(RLYMDE); #endif +//Use open drain (floating pin) when relay should be off +#ifndef RLYODRAIN +WLED_GLOBAL bool rlyOpenDrain _INIT(false); +#else +WLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN); +#endif #ifndef IRPIN #define IRPIN -1 #endif @@ -348,6 +360,11 @@ WLED_GLOBAL bool useGlobalLedBuffer _INIT(true); // double buffering enabled on #endif WLED_GLOBAL bool correctWB _INIT(false); // CCT color correction of RGB color WLED_GLOBAL bool cctFromRgb _INIT(false); // CCT is calculated from RGB instead of using seg.cct +#ifdef WLED_USE_IC_CCT +WLED_GLOBAL bool cctICused _INIT(true); // CCT IC used (Athom 15W bulbs) +#else +WLED_GLOBAL bool cctICused _INIT(false); // CCT IC used (Athom 15W bulbs) +#endif WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on colors WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness WLED_GLOBAL float gammaCorrectVal _INIT(2.8f); // gamma correction value @@ -692,7 +709,6 @@ WLED_GLOBAL uint16_t olen _INIT(0); WLED_GLOBAL size_t fsBytesUsed _INIT(0); WLED_GLOBAL size_t fsBytesTotal _INIT(0); WLED_GLOBAL unsigned long presetsModifiedTime _INIT(0L); -WLED_GLOBAL JsonDocument* fileDoc; WLED_GLOBAL bool doCloseFile _INIT(false); // presets @@ -705,7 +721,8 @@ WLED_GLOBAL byte optionType; WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers -WLED_GLOBAL bool doPublishMqtt _INIT(false); + +WLED_GLOBAL bool psramSafe _INIT(true); // is it safe to use PSRAM (on ESP32 rev.1; compiler fix used "-mfix-esp32-psram-cache-issue") // status led #if defined(STATUSLED) @@ -782,7 +799,7 @@ WLED_GLOBAL int8_t spi_sclk _INIT(SPISCLKPIN); #endif // global ArduinoJson buffer -#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) +#if defined(ARDUINO_ARCH_ESP32) WLED_GLOBAL JsonDocument *pDoc _INIT(nullptr); #else WLED_GLOBAL StaticJsonDocument gDoc; diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 4ce113cb9e..d184e98789 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,36 +18,6 @@ static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security setti static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; -static const char s_javascript[] PROGMEM = "application/javascript"; -static const char s_json[] = "application/json"; // AsyncJson-v6.h -static const char s_html[] PROGMEM = "text/html"; -static const char s_plain[] = "text/plain"; // Espalexa.h -static const char s_css[] PROGMEM = "text/css"; -static const char s_png[] PROGMEM = "image/png"; -static const char s_gif[] PROGMEM = "image/gif"; -static const char s_jpg[] PROGMEM = "image/jpeg"; -static const char s_ico[] PROGMEM = "image/x-icon"; -//static const char s_xml[] PROGMEM = "text/xml"; -//static const char s_pdf[] PROGMEM = "application/x-pdf"; -//static const char s_zip[] PROGMEM = "application/x-zip"; -//static const char s_gz[] PROGMEM = "application/x-gzip"; - -String getFileContentType(String &filename) { - if (filename.endsWith(F(".htm"))) return FPSTR(s_html); - else if (filename.endsWith(F(".html"))) return FPSTR(s_html); - else if (filename.endsWith(F(".css"))) return FPSTR(s_css); - else if (filename.endsWith(F(".js"))) return FPSTR(s_javascript); - else if (filename.endsWith(F(".json"))) return s_json; - else if (filename.endsWith(F(".png"))) return FPSTR(s_png); - else if (filename.endsWith(F(".gif"))) return FPSTR(s_gif); - else if (filename.endsWith(F(".jpg"))) return FPSTR(s_jpg); - else if (filename.endsWith(F(".ico"))) return FPSTR(s_ico); -// else if (filename.endsWith(F(".xml"))) return FPSTR(s_xml); -// else if (filename.endsWith(F(".pdf"))) return FPSTR(s_pdf); -// else if (filename.endsWith(F(".zip"))) return FPSTR(s_zip); -// else if (filename.endsWith(F(".gz"))) return FPSTR(s_gz); - return s_plain; -} //Is this an IP? static bool isIp(String str) { @@ -183,7 +153,7 @@ static String msgProcessor(const String& var) static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { if (!correctPIN) { - if (final) request->send(401, FPSTR(s_plain), FPSTR(s_unlock_cfg)); + if (final) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg)); return; } if (!index) { @@ -204,10 +174,10 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, request->_tempFile.close(); if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash doReboot = true; - request->send(200, FPSTR(s_plain), F("Configuration restore successful.\nRebooting...")); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); } else { if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes(); - request->send(200, FPSTR(s_plain), F("File Uploaded!")); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); } cacheInvalidate++; } @@ -259,24 +229,24 @@ void initServer() #ifdef WLED_ENABLE_WEBSOCKETS #ifndef WLED_DISABLE_2D - server.on(SET_F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveviewws2D, PAGE_liveviewws2D_length); + server.on(F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveviewws2D, PAGE_liveviewws2D_length); }); #endif #endif - server.on(SET_F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveview, PAGE_liveview_length); + server.on(F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length); }); //settings page - server.on(SET_F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); }); // "/settings/settings.js&p=x" request also handled by serveSettings() static const char _style_css[] PROGMEM = "/style.css"; server.on(_style_css, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(s_css), PAGE_settingsCss, PAGE_settingsCss_length); + handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(CONTENT_TYPE_CSS), PAGE_settingsCss, PAGE_settingsCss_length); }); static const char _favicon_ico[] PROGMEM = "/favicon.ico"; @@ -287,28 +257,29 @@ void initServer() static const char _skin_css[] PROGMEM = "/skin.css"; server.on(_skin_css, HTTP_GET, [](AsyncWebServerRequest *request) { if (handleFileRead(request, FPSTR(_skin_css))) return; - AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(s_css)); + AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(CONTENT_TYPE_CSS)); request->send(response); }); - server.on(SET_F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); }); - server.on(SET_F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129); doReboot = true; }); - server.on(SET_F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){ + server.on(F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){ serveSettings(request, true); }); - server.on(SET_F("/json"), HTTP_GET, [](AsyncWebServerRequest *request){ + const static char _json[] PROGMEM = "/json"; + server.on(FPSTR(_json), HTTP_GET, [](AsyncWebServerRequest *request){ serveJson(request); }); - AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(F("/json"), [](AsyncWebServerRequest *request) { + AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(FPSTR(_json), [](AsyncWebServerRequest *request) { bool verboseResponse = false; bool isConfig = false; @@ -356,33 +327,33 @@ void initServer() doSerializeConfig = true; //serializeConfig(); //Save new settings to FS } } - request->send(200, s_json, F("{\"success\":true}")); + request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}")); }, JSON_BUFFER_SIZE); server.addHandler(handler); - server.on(SET_F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)VERSION); + server.on(F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)VERSION); }); - server.on(SET_F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)millis()); + server.on(F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)millis()); }); - server.on(SET_F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send(200, FPSTR(s_plain), (String)ESP.getFreeHeap()); + server.on(F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)ESP.getFreeHeap()); }); #ifdef WLED_ENABLE_USERMOD_PAGE server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_usermod, PAGE_usermod_length); + handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length); }); #endif - server.on(SET_F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){ + server.on(F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){ serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254); }); - server.on(SET_F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, + server.on(F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {handleUpload(request, filename, index, data, len, final);} ); @@ -453,7 +424,7 @@ void initServer() #ifdef WLED_ENABLE_DMX server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ - request->send_P(200, FPSTR(s_html), PAGE_dmxmap , dmxProcessor); + request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap , dmxProcessor); }); #else server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ @@ -464,7 +435,7 @@ void initServer() server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { if (captivePortal(request)) return; if (!showWelcomePage || request->hasArg(F("sliders"))) { - handleStaticContent(request, F("/index.htm"), 200, FPSTR(s_html), PAGE_index, PAGE_index_L); + handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L); } else { serveSettings(request); } @@ -473,20 +444,20 @@ void initServer() #ifdef WLED_ENABLE_PIXART static const char _pixart_htm[] PROGMEM = "/pixart.htm"; server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(s_html), PAGE_pixart, PAGE_pixart_L); + handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L); }); #endif #ifndef WLED_DISABLE_PXMAGIC static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm"; server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(s_html), PAGE_pxmagic, PAGE_pxmagic_L); + handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L); }); #endif static const char _cpal_htm[] PROGMEM = "/cpal.htm"; server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) { - handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(s_html), PAGE_cpal, PAGE_cpal_L); + handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L); }); #ifdef WLED_ENABLE_WEBSOCKETS @@ -511,7 +482,7 @@ void initServer() #ifndef WLED_DISABLE_ALEXA if(espalexa.handleAlexaApiCall(request)) return; #endif - handleStaticContent(request, request->url(), 404, FPSTR(s_html), PAGE_404, PAGE_404_length); + handleStaticContent(request, request->url(), 404, FPSTR(CONTENT_TYPE_HTML), PAGE_404, PAGE_404_length); }); } @@ -522,7 +493,7 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h messageSub = subl; optionType = optionT; - request->send_P(code, FPSTR(s_html), PAGE_msg, msgProcessor); + request->send_P(code, FPSTR(CONTENT_TYPE_HTML), PAGE_msg, msgProcessor); } @@ -530,7 +501,7 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro { AsyncJsonResponse *response = new AsyncJsonResponse(64); if (error < ERR_NOT_IMPL) response->addHeader(F("Retry-After"), F("1")); - response->setContentType(s_json); + response->setContentType(CONTENT_TYPE_JSON); response->setCode(code); JsonObject obj = response->getRoot(); obj[F("error")] = error; @@ -546,12 +517,12 @@ void serveSettingsJS(AsyncWebServerRequest* request) byte subPage = request->arg(F("p")).toInt(); if (subPage > 10) { strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');")); - request->send(501, FPSTR(s_javascript), buf); + request->send(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); return; } if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) { strcpy_P(buf, PSTR("alert('PIN incorrect.');")); - request->send(401, FPSTR(s_javascript), buf); + request->send(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); return; } strcat_P(buf,PSTR("function GetV(){var d=document;")); @@ -559,7 +530,7 @@ void serveSettingsJS(AsyncWebServerRequest* request) strcat_P(buf,PSTR("}")); AsyncWebServerResponse *response; - response = request->beginResponse(200, FPSTR(s_javascript), buf); + response = request->beginResponse(200, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf); response->addHeader(F("Cache-Control"), F("no-store")); response->addHeader(F("Expires"), F("0")); request->send(response); @@ -640,7 +611,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { } int code = 200; - String contentType = FPSTR(s_html); + String contentType = FPSTR(CONTENT_TYPE_HTML); const uint8_t* content; size_t len; @@ -666,7 +637,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { return; } case SUBPAGE_PINREQ : content = PAGE_settings_pin; len = PAGE_settings_pin_length; code = 401; break; - case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(s_css); break; + case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(CONTENT_TYPE_CSS); break; case SUBPAGE_JS : serveSettingsJS(request); return; case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break; default: content = PAGE_settings; len = PAGE_settings_length; break; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 1dd141a688..307a0959e1 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -55,7 +55,7 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp } else { verboseResponse = deserializeState(root); } - releaseJSONBufferLock(); // will clean fileDoc + releaseJSONBufferLock(); if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon if (verboseResponse) { @@ -102,7 +102,6 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp void sendDataWs(AsyncWebSocketClient * client) { if (!ws.count()) return; - AsyncWebSocketMessageBuffer * buffer; if (!requestJSONBufferLock(12)) { if (client) { @@ -129,7 +128,7 @@ void sendDataWs(AsyncWebSocketClient * client) return; } #endif - buffer = ws.makeBuffer(len); // will not allocate correct memory sometimes on ESP8266 + AsyncWebSocketBuffer buffer(len); #ifdef ESP8266 size_t heap2 = ESP.getFreeHeap(); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); @@ -141,23 +140,18 @@ void sendDataWs(AsyncWebSocketClient * client) DEBUG_PRINTLN(F("WS buffer allocation failed.")); ws.closeAll(1013); //code 1013 = temporary overload, try again later ws.cleanupClients(0); //disconnect all clients to release memory - ws._cleanBuffers(); return; //out of memory } - - buffer->lock(); - serializeJson(*pDoc, (char *)buffer->get(), len); + serializeJson(*pDoc, (char *)buffer.data(), len); DEBUG_PRINT(F("Sending WS data ")); if (client) { - client->text(buffer); + client->text(std::move(buffer)); DEBUG_PRINTLN(F("to a single client.")); } else { - ws.textAll(buffer); + ws.textAll(std::move(buffer)); DEBUG_PRINTLN(F("to multiple clients.")); } - buffer->unlock(); - ws._cleanBuffers(); releaseJSONBufferLock(); } @@ -187,11 +181,10 @@ bool sendLiveLedsWs(uint32_t wsClient) #endif size_t bufSize = pos + (used/n)*3; - AsyncWebSocketMessageBuffer * wsBuf = ws.makeBuffer(bufSize); + AsyncWebSocketBuffer wsBuf(bufSize); if (!wsBuf) return false; //out of memory - uint8_t* buffer = wsBuf->get(); + uint8_t* buffer = reinterpret_cast(wsBuf.data()); if (!buffer) return false; //out of memory - wsBuf->lock(); // protect buffer from being cleaned by another WS instance buffer[0] = 'L'; buffer[1] = 1; //version @@ -218,9 +211,7 @@ bool sendLiveLedsWs(uint32_t wsClient) buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B } - wsc->binary(wsBuf); - wsBuf->unlock(); // un-protect buffer - ws._cleanBuffers(); + wsc->binary(std::move(wsBuf)); return true; } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 04e0ebfdf5..f44b6b8946 100755 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -145,10 +145,13 @@ void appendGPIOinfo() { oappend(SET_F("d.rsvd=[22,23,24,25,26,27,28,29,30,31,32")); #elif defined(CONFIG_IDF_TARGET_ESP32S3) oappend(SET_F("d.rsvd=[19,20,22,23,24,25,26,27,28,29,30,31,32")); // includes 19+20 for USB OTG (JTAG) + if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH -seems that octal PSRAM is very common on S3. #elif defined(CONFIG_IDF_TARGET_ESP32C3) oappend(SET_F("d.rsvd=[11,12,13,14,15,16,17")); #elif defined(ESP32) oappend(SET_F("d.rsvd=[6,7,8,9,10,11,24,28,29,30,31,37,38")); + if (!pinManager.isPinOk(16,false)) oappend(SET_F(",16")); // covers PICO & WROVER + if (!pinManager.isPinOk(17,false)) oappend(SET_F(",17")); // covers PICO & WROVER #else oappend(SET_F("d.rsvd=[6,7,8,9,10,11")); #endif @@ -163,14 +166,6 @@ void appendGPIOinfo() { //Note: Using pin 3 (RX) disables Adalight / Serial JSON - #if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if (psramFound()) oappend(SET_F(",16,17")); // GPIO16 & GPIO17 reserved for SPI RAM on ESP32 (not on S2, S3 or C3) - #elif defined(CONFIG_IDF_TARGET_ESP32S3) - if (psramFound()) oappend(SET_F(",33,34,35,36,37")); // in use for "octal" PSRAM or "octal" FLASH -seems that octal PSRAM is very common on S3. - #endif - #endif - #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { for (uint8_t p=0; p")); + oappend((char*)FPSTR(releaseString)); oappend(SET_F("
(")); #if defined(ARDUINO_ARCH_ESP32) oappend(ESP.getChipModel()); From d01542d0adfd3643d5aecc795316d5f64ac515db Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 1 May 2024 14:10:36 +0200 Subject: [PATCH 12/21] major bugfix, hopefully resolves all crashes - uncought out of bounds memory access in rendering and a bug in pointer assignment caused many hard to track down crashes. should be fixed now. - some minor fixes as well --- wled00/FX.cpp | 73 +++++++++++++++------------------ wled00/FXparticleSystem.cpp | 81 ++++++++++++++++++++++--------------- wled00/FXparticleSystem.h | 5 +-- 3 files changed, 83 insertions(+), 76 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 70e4747167..c0fc30c747 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7945,7 +7945,7 @@ uint16_t mode_particlevortex(void) PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].sat = 230; - PartSys->particles[PartSys->numParticles - i].ttl = 100; //set alive + PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change @@ -8078,9 +8078,9 @@ uint16_t mode_particlefireworks(void) numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { - PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon - PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched - } + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8167,6 +8167,14 @@ uint16_t mode_particlefireworks(void) } else { + /* + if( PartSys->sources[j].source.vy < 0) //explosion is ongoing + { + if(i < (emitparticles>>2)) //set 1/4 of particles to larger size + PartSys->sources[j].size = 50+random16(140); + else + PartSys->sources[j].size = 0; + }*/ PartSys->sprayEmit(PartSys->sources[j]); if ((j % 3) == 0) { @@ -8250,7 +8258,8 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.perpetural = true; // source never dies } } else @@ -8287,8 +8296,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers - PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan + PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) @@ -8321,7 +8329,6 @@ uint16_t mode_particlefire(void) if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise - // initialize the flame sprays numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8354,7 +8361,7 @@ uint16_t mode_particlefire(void) } uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) - numFlames = min((uint32_t)PartSys->numSources, (1 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + numFlames = min((uint32_t)PartSys->numSources, (2 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 uint32_t percycle = numFlames*2/3;// / 2; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) // percycle = map(SEGMENT.intensity,0,255, 2, (numFlames*3) / 2); //TODO: does this give better flames or worse? @@ -8421,7 +8428,7 @@ uint16_t mode_particlefire(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,o1=1"; +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Base Heat,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; /* PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce @@ -8706,7 +8713,6 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix - //PartSys->sources[i].source.ttl = 255; // source never dies, replenish its lifespan TODO: source is not moved? PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers } @@ -8770,6 +8776,7 @@ uint16_t mode_particlebox(void) for (i = 0; i < maxnumParticles; i++) { PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].perpetural = true; //never dies PartSys->particles[i].hue = i * 5; // color range PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction @@ -8791,16 +8798,12 @@ uint16_t mode_particlebox(void) { xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); - // scale the gravity force - //xgravity /= 1 + ((256 - SEGMENT.custom1) >> 5); - //ygravity /= 1 + ((256 - SEGMENT.custom1) >> 5); // scale the gravity force - xgravity = (xgravity * SEGMENT.custom1) / 50; - ygravity = (ygravity * SEGMENT.custom1) / 50; + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; } else //go in a circle - { - // PartSys->applyAngleForce(PartSys->particles, PartSys->usedParticles, SEGMENT.custom1>>2, SEGMENT.aux0<<8);//not used, calculate here directly as perlin noise force is in x and y, not angle + { xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } @@ -8811,15 +8814,6 @@ uint16_t mode_particlebox(void) } PartSys->applyForce(xgravity, ygravity); - - // reset particle TTL so they never die - for (i = 0; i < PartSys->usedParticles; i++) - { - if (PartSys->particles[i].ttl > 0) - { - PartSys->particles[i].ttl = 500; // particles never die - } - } } if (SEGMENT.call % (32-SEGMENT.custom3) == 0) @@ -9016,7 +9010,7 @@ uint16_t mode_particleimpact(void) } } } - else if ( PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + else if (PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { // reinitialize meteor PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top @@ -9024,7 +9018,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.vy = -random16(30) - 30; // meteor downward speed PartSys->sources[i].source.vx = random(30) - 15; PartSys->sources[i].source.hue = random16(); // random color - PartSys->sources[i].source.ttl = 2000; // long life, will explode at bottom + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; @@ -9063,7 +9057,8 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].source.hue = random16(); PartSys->sources[0].source.vx = -7; PartSys->sources[0].source.collide = true; // seeded particles will collide - PartSys->sources[0].source.ttl = 100; //is replenished below, it never dies + //PartSys->sources[0].source.ttl = 100; //TODO: remove, is now done in PS init + PartSys->sources[0].source.perpetural = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -9103,25 +9098,23 @@ uint16_t mode_particleattractor(void) { attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; + attractor->ttl = 100; + attractor->perpetural = true; } //set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly - { - attractor->ttl = 100; //must be alive to move - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor - } + PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + } else{ attractor->x = PartSys->maxX >> 1; // set to center attractor->y = PartSys->maxY >> 1; } - if (SEGMENT.call % 5 == 0) - { + if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; - PartSys->sources[0].source.ttl = 100; //spray never dies - } + SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); @@ -9479,7 +9472,7 @@ uint16_t mode_particlghostrider(void) //PartSys->setColorByAge(SEGMENT.check1); PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only - //color by age (PS always starts with hue = 255 so cannot use that) + //color by age (PS color by age always starts with hue = 255 so cannot use that) if(SEGMENT.check1) { for(int i = 0; i < PartSys->usedParticles; i++) @@ -9504,7 +9497,7 @@ uint16_t mode_particlghostrider(void) //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; - PartSys->particles[PartSys->usedParticles-1].ttl = PartSys->sources[0].source.ttl; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; PartSys->particles[PartSys->usedParticles-1].sat = 0; //emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 64fee78ccc..3a0eb31252 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -63,6 +63,7 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero for (int i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive } for (int i = 0; i < numParticles; i++) { @@ -304,8 +305,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if (part.ttl > 0) { - // age - part.ttl--; + + if(!part.perpetural) + part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl @@ -318,7 +320,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P { if(advancedproperties->size > 0) usesize = true; //note: variable eases out of frame checking below - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + advancedproperties->size); + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view if (options.bounceX) @@ -338,7 +340,7 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P bool isleaving = true; if(usesize) //using individual particle size { - if (((newX > -particleHardRadius) || (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough isleaving = false; } @@ -644,6 +646,7 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { + CRGB baseRGB; bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) CRGB **framebuffer = NULL; //local frame buffer @@ -653,14 +656,26 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) { + cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation + /* + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { + sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ + if(advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it + } + sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -674,11 +689,9 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } } } - if(advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it - } + } + } if(!useLocalBuffer) //disabled or allocation above failed @@ -717,14 +730,8 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = (CRGB)baseHSV; //convert back to RGB } } - - if(renderbuffer) //set buffer to zero if it exists - { - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // renderbuffer is 10x10 pixels. note: passing the buffer and setting it zero here is faster than creating a new buffer for every particle - } - - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); } if(particlesize > 0) @@ -791,7 +798,10 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if(advPartProps[particleindex].size > 0) { if(renderbuffer) - advancedrender = true; + { + advancedrender = true; + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels + } else return; //cannot render without buffer, advanced size particles are allowed out of frame } @@ -869,8 +879,6 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (pxlbrightness[3] >= 0) pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - - if(advancedrender) { //render particle to a bigger size @@ -883,6 +891,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... uint32_t rendersize = 4; uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer + //TODO: add asymmetrical size support blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 if (advPartProps[particleindex].size > 64) { @@ -913,20 +922,26 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { xfb = xfb_orig + xrb; if(xfb > maxXpixel) + { if (particlesettings.wrapX) // wrap x to the other side if required xfb = xfb % (maxXpixel + 1); else continue; + } + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) { yfb = yfb_orig + yrb; if(yfb > maxYpixel) - if (particlesettings.wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); - else - continue; + { + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + } + //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); //TODO: this is just a test, need to render to correct coordinates with out of frame checking + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); } } } @@ -1362,7 +1377,7 @@ int32_t ParticleSystem::limitSpeed(int32_t speed) // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { - CRGB ** array2D = (CRGB **)malloc(cols * sizeof(CRGB *) + cols * rows * sizeof(CRGB)); + CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); if (array2D == NULL) DEBUG_PRINT(F("PS buffer alloc failed")); else @@ -1373,7 +1388,7 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { array2D[i] = start + i * rows; } - memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero + //memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero (TODO: remove, not needed if calloc is used) } return array2D; } @@ -1395,7 +1410,7 @@ void ParticleSystem::updateSystem(void) void ParticleSystem::updatePSpointers(bool isadvanced) { //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p\n"), this); + //DEBUG_PRINTF_P(PSTR("this PS %p "), this); //Note on memory alignment: //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. @@ -1403,19 +1418,19 @@ void ParticleSystem::updatePSpointers(bool isadvanced) particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) if(isadvanced) { - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - advPartProps = reinterpret_cast(sources + numParticles); + advPartProps = reinterpret_cast(particles + numParticles); + sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) } else { - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - advPartProps = NULL; + advPartProps = NULL; + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) } PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - //DEBUG_PRINTF_P(PSTR("particles %p\n"), particles); - //DEBUG_PRINTF_P(PSTR("sources %p\n"), sources); - //DEBUG_PRINTF_P(PSTR("adv. props %p\n"), advPartProps); + //DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + //DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + //DEBUG_PRINTF_P(PSTR(" adv. props %p\n"), advPartProps); //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index dacf6ab5b1..7413dccedc 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -58,14 +58,13 @@ typedef struct { uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions - bool flag3 : 1; // unused flags... - bool flag4 : 1; + bool perpetural : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool flag4 : 1; // unused flag } PSparticle; // struct for additional particle settings (optional) typedef struct { - uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles From a8ad5b1087732753f935394b1057c9974264ba54 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 5 May 2024 22:09:47 +0200 Subject: [PATCH 13/21] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit ba337abb548c530042713d2d46373d36bfa7204a Merge: 0b45f665 3f9a6cae Author: Damian Schneider Date: Sun May 5 21:57:32 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 0b45f6652a619cdb315d437fb8dd9c0038f4b792 Author: Damian Schneider Date: Sun May 5 21:56:19 2024 +0200 bugfix, sprayEmit() would not return and get stuck - forgot default return value... commit 255815605608a7fcacc2aa5264aeefb95d8bb4bb Author: Damian Schneider Date: Sun May 5 11:19:02 2024 +0200 code cleanup, removed some unused stuff commit ac5399aa26f403dbb0c7ab05163c0bd9d0d60f6b Author: Damian Schneider Date: Sun May 5 08:18:43 2024 +0200 Added advanced particle size control and new Blob FX - advanced size control allows for growing, shrinking, wobbling - render function updated to support asymmetric rendering - various code improvements and bugfixes - some FX parameter tuning - bugfix: removed sli() sei() calls in render function that caused random flickering on S3/C3 (may add that back in but only for ESP8266 to reduce fragmentation) - removed some debug / test stuff commit 3f9a6cae53889898486dae727bbacebc680d6ee0 Author: Frank Date: Sat May 4 14:34:23 2024 +0200 AR: fix for arduinoFFT 2.x API in contrast to previous 'dev' versions, the storage for windowWeighingFactors is now managed internally by the arduinoFFT object. commit cd5494fdd2040ba8d6858532b7348b082c345ebb Author: Frank Date: Sat May 4 13:36:56 2024 +0200 AR pin config: SCK == 1 --> PDM microphone commit 5ab1b14d6b32b1aef17ee01b334205d42d98d832 Merge: fa76431d 379f1813 Author: Blaž Kristan Date: Fri May 3 23:25:36 2024 +0200 Merge pull request #3946 from freakintoddles2/0_15 Adds an API parameter to allow the user to skip to the next preset in a playlist at any time commit 379f1813620a56bb0b3136315feb647fb0c3d45d Author: Todd Meyer Date: Fri May 3 11:51:47 2024 -0700 Further simplification commit dd19aa63d0e15693f0666ea1e33a370677b88450 Author: Todd Meyer Date: Fri May 3 08:47:14 2024 -0700 Forgot F[], added it commit 6df3b417a94899fd382708208ced9ab08845826c Author: Todd Meyer Date: Fri May 3 08:30:37 2024 -0700 Updated based on more feedback commit fa76431dd673e5c1a24c75fe7d9348a93e3f22f5 Author: Blaz Kristan Date: Fri May 3 16:08:20 2024 +0200 Changelog update commit 6504fb68b6ba3fc49d18594df78812c6e153e9bc Author: Blaz Kristan Date: Fri May 3 15:46:16 2024 +0200 Minor MQTT optimisation. commit 2ff49cf657a322d17ec4fab854d9834ae10a5566 Author: Blaz Kristan Date: Fri May 3 15:45:15 2024 +0200 Fix for #3952 - included IR optimisations & code rearrangement commit fa1aa1fe805aaf5f91bfe6b8f7d237e494ef643c Merge: 85b95a29 22f6128b Author: Blaž Kristan Date: Fri May 3 09:56:14 2024 +0200 Merge pull request #3944 from paspiz85/pas4 Using brightness in analog clock overlay commit 85b95a2940b5c17bf3d37b53ae945bc121f3b1c1 Merge: 4df936a4 736a8b1b Author: Blaž Kristan Date: Fri May 3 09:53:00 2024 +0200 Merge pull request #3945 from w00000dy/Webpage-shortcuts Add Webpage shortcuts and Fix resizing bug commit 5e38039c4dd630d7b4c6841deb5e0b04aa07f573 Author: Todd Meyer Date: Thu May 2 14:36:18 2024 -0700 Updated based on more feedback commit 4df936a437a2e643fca724885b336b6bb3034bbd Author: Blaz Kristan Date: Thu May 2 10:33:10 2024 +0200 Fix for unfortunate prior CRLF coversion. commit 736a8b1b80102d2f9b32b8cc88ce5f409eeca116 Author: Blaz Kristan Date: Thu May 2 10:31:50 2024 +0200 Fix for rotating tablet into PC mode. commit 22f6128bc47c7ee7349b4f039758bf46962b0eba Author: Pasquale Pizzuti Date: Thu May 2 09:04:07 2024 +0200 using global brightness commit db475b69988567f19c3969d1808c96918924f55e Author: freakintoddles2 Date: Wed May 1 10:09:17 2024 -0700 Update playlist.cpp commit 6daf7f6322eacdc0ff83756b4da735d02a269fa9 Author: freakintoddles2 Date: Wed May 1 10:07:52 2024 -0700 Update wled.cpp reworked based on PR feedback commit 16086c09615d7e8c2e2050c650b9af00b875062c Author: freakintoddles2 Date: Wed May 1 10:05:26 2024 -0700 Update wled.h reworked based on feedback from original PR commit e88c81ad0d6a1ff8c18667facd2b7b326ede2b74 Author: freakintoddles2 Date: Wed May 1 10:04:02 2024 -0700 Update set.cpp reworked based on feedback commit a2b9aed40df5bb55eb4f53db72c6871c72c9b30d Author: freakintoddles2 Date: Wed May 1 10:03:16 2024 -0700 Update playlist.cpp reworked approach based on feedback commit caa4fe1ec4f814f89fa2c77ef7405b0935b846ec Author: freakintoddles2 Date: Wed May 1 10:02:27 2024 -0700 Update json.cpp reworked approach based on feedback commit 25fb878e5434dd47b888bd3e5a46176c369ff8c4 Author: freakintoddles2 Date: Wed May 1 10:01:30 2024 -0700 Update fcn_declare.h reworked approach based on feedback commit 77167a28546443c1e8be84f0713f40b227a64285 Author: Damian Schneider Date: Wed May 1 13:53:59 2024 +0200 Found and fixed crashes, it was wrongly assigned pointers commit 3527144b94b61ba46134e635dd05294c14980b87 Author: Damian Schneider Date: Wed May 1 12:08:03 2024 +0200 Fixed another memory / pointer bug, but there is still one left... -also some minor fixes commit 06ae14c0d6766239b7776d6f8cadd37c5865eaf7 Author: Damian Schneider Date: Wed May 1 07:07:48 2024 +0200 added 'perpetual' flag to particles commit a1d6ffadad852449a825431ad0bb8bba2f7f448d Author: freakintoddles2 Date: Tue Apr 30 16:49:52 2024 -0700 Update json.cpp adds support for np boolean parameter to skip to next preset commit 3b89814b6935261d65f95059690591f51a0eab5f Author: freakintoddles2 Date: Tue Apr 30 16:33:30 2024 -0700 Update set.cpp added new NP command to API to allow user to skip to next preset in a playlist. Example use is win&NP in a preset. commit 071e0be445ed012ff9ab1f72347440684eed678f Author: freakintoddles2 Date: Tue Apr 30 16:23:43 2024 -0700 Update fcn_declare.h Updated to add the optional skipNext bool to handlePlaylist() which allows people to skip to the next preset when desired commit bed364d75e8ecc0f65c43bfb1db4d9a089072158 Author: freakintoddles2 Date: Tue Apr 30 16:21:40 2024 -0700 Update playlist.cpp Updated to allow a user to optionally skip to the next preset in the playlist anytime they desire. commit d2984e9e160f649e0d900761d76240740201a4b6 Author: Woody Date: Tue Apr 30 18:57:53 2024 +0200 add Webpage shortcuts, resolves #2362 commit fd9570e7826b53a0c59440179bd340f195d0bb6a Author: Pasquale Pizzuti Date: Tue Apr 30 17:52:35 2024 +0200 using color_fade commit ff10130176ff06a81ed09113b6669703583a3c71 Author: Woody Date: Tue Apr 30 16:53:47 2024 +0200 Fix resizing bug The bug was that when resizing the window, it always jumped to the Colors tab instead of staying on the currently selected tab. commit c7d292a7165e81e0c0429b4f94e3e401b4e3dad0 Author: Pasquale Pizzuti Date: Tue Apr 30 14:09:12 2024 +0200 using brightness in analog clock overlay commit 5472f76a3dd08e0e76eb52f17160e639fb43b6ca Merge: a6bf5b60 9f99a189 Author: Damian Schneider Date: Tue Apr 30 07:50:44 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit a6bf5b603d9f7e5f4184f01345047da95a0acd1f Author: Damian Schneider Date: Tue Apr 30 07:41:04 2024 +0200 added walls to ghostride, fixed some bugs commit 33ede416e5e430e70e49cc037f338e8238111492 Author: Damian Schneider Date: Mon Apr 29 19:46:19 2024 +0200 Replaced Ghost Rider FX with PS version - new FX is kept close to original animation but added more user settings commit 2e0cb3a49fb9dbfa3619c6bbcf31c01a8cd8585d Author: Damian Schneider Date: Sun Apr 28 19:29:57 2024 +0200 Fixed some nasty memory bugs, fixed some FX parameters - fixed memory alignment bug in PS pointer assignment by making sure only multiples of 4 are allowed for particles and sources - added saturation back to particle struct, as it was aligned to 10 bytes anyway. - fixed a bug where a null pointer could be accessed - fixed rendering out of frame particles if buffer allocation failed - improvements on ESP8266 commit 3eb94ebffa92a0a3f9823537028fe218ce4ddd15 Merge: 53607d6c 8110259d Author: Damian Schneider Date: Sun Apr 28 11:30:44 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 53607d6cdb0953af3c330b3d4da74d33eeb949a2 Author: Damian Schneider Date: Sun Apr 28 11:25:57 2024 +0200 added individual size particle rendering plus some fixes - advanced particles can now be rendered to individual sizes. It is computationally intensive but it works well for up to 15 very large particles and more smaller ones - added collision handling for individual sizes (walls and collisions) - fixed bugs in particlebox - fixed fire not transitioning properly (flickering) when frame skip is active - removed 'wraparound' function as it can easily be done by casting to an unsigned and then modulo - fixed ballpit particles wandering left and right very fast if wrapX is set commit 7abc440f90a18538ac277e062c86c7dca4d13615 Author: Damian Schneider Date: Sat Apr 27 13:40:38 2024 +0200 removed zero inits - removed zero initialisations in FX, segment.data is set to zero by alloc function commit 54e94ddb91554302d7b5717a01ac99f3b7089970 Author: Damian Schneider Date: Sat Apr 27 13:18:30 2024 +0200 Bugfixes, improvements and added wall roughness setting - fixed bug in PS fuzzy noise which made it asymmetric for some reason, seems to work better now - added particle size option to attractor but had to remove speed setting (now fixed emit speed) - some parameter tuning of FX - improvements to code size in render function - added smear option to blurring (not tested much, may be buggy without smear) - speed improvement to caldForce_dv, added zero checking. commit a22466a852f4aeedfa7a77994c67c0ff016cad37 Merge: 3386a844 004b1c3e Author: Damian Schneider Date: Fri Apr 26 08:56:07 2024 +0200 Merge branch 'FXparticleSystem' of https://github.com/DedeHai/WLED into FXparticleSystem commit 3386a844eb156a1f82c2d25e67360b74241afbe2 Author: Damian Schneider Date: Fri Apr 26 08:55:17 2024 +0200 added local render blurring, boosting FPS, work in progress - changed fast-add function to use pointers - added fast-scaling function - added simple (but fast) 2D blurring function test shows that local blurring of full frame is almost double the speed (40FPS now is 80FPS). lots of comments still there commit 004b1c3eaa0522ce58219a8d682c35eb2dd399f5 Merge: e43f3bd5 e83d3cb4 Author: Damian Schneider Date: Thu Apr 25 15:50:07 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit e43f3bd5c612379c7004db005cd90e688f2036de Author: Damian Schneider Date: Wed Apr 24 17:33:02 2024 +0200 bugfix in wrap function and firwork FX commit 2e7fbc0310321f223a808a02a2c739adcc658d43 Author: Damian Schneider Date: Wed Apr 24 06:39:30 2024 +0200 debugging going on commit 7b68946c023f64dc1dd0da996dfa573b2e303864 Author: Damian Schneider Date: Mon Apr 22 18:52:13 2024 +0200 in the middle of fixing FX to use new PS functions commit e01781407734db9c12332b8ee4c37c3977660298 Author: Damian Schneider Date: Sat Apr 20 15:34:16 2024 +0200 added more tests, non compiling at the moment commit 856527b447d19ae4596b0b40ba92d98c87d92cc5 Author: Damian Schneider Date: Sat Apr 20 12:10:24 2024 +0200 work in progress, added test function to increase particle size, also added wobbling test commit 4146ff4233af5b2c48a2f08bc5c4811fbe2a5782 Merge: 50489f73 d126611e Author: Damian Schneider Date: Tue Apr 16 19:32:14 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 50489f7364f4f312a940c368dce5fda6316366a0 Author: Damian Schneider Date: Tue Apr 16 19:29:05 2024 +0200 work in progress, added motion blur and line attracto (non working) commit f7337b9b71a792251e65e73459780e5fa733033f Merge: 5e2dca35 d18f078b Author: Damian Schneider Date: Sun Apr 7 13:39:23 2024 +0200 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 5e2dca35772dd8376b2d015ff5973897366ab1a3 Author: Damian Schneider Date: Sat Apr 6 07:15:51 2024 +0200 Fixed Speed limit limiting speed was incorrect, leading to overflows. fixed this. also fixed bugs in GEQ, removed some debug stuff, added FPS limit to fire (just en experiment) commit 0a251aefbe7f50bf071a1ee8e11b1640c163d8d9 Author: Damian Schneider Date: Thu Apr 4 17:51:32 2024 +0200 another huge update, many improvements, fine-tune collision, fire and some other FX -removed classic fire render as palette now looks much better -tweaked fire parameters to more awesome -added (optional) turbulence to fire using perlin-noise -ported spray FX to use PS-class -now definitely fixed asymmetrical collision issue: do not use bitshifts on negative numbers! -changed piling behaviour of particles, full rework. looks way more natural now and works much better -changed bouncing behavour: they now bounce at full diameter, making them stay fully in frame when laying on the floor -replaced all relevant bitshifts with divisions for higher accuracy throughout the PS -added new modes to particle box FX -changed a lot of FX parameters (finetuning) -changed all config strings to proper settings (matrix only) -fixed newly introduced bugs -added speedup/slowdown to vortex FX (aka candy, aka rotating sprays) -some renaming -fixed bugs… lots of bugs -merged rendering functions, removed obsolete stuff commit 32343eaa757de7656cf0814e9a0f16eabb3feb7d Author: Damian Schneider Date: Tue Apr 2 20:26:24 2024 +0200 updated fire, added some functions to PS, ported attractor FX - added turbulance to fire (after hours of fine-tuning) it now looks even more awesome - added attractor animation back in and improved it with more functionality - fixed the attractor function in the PS - renamed FX: 'candy' is now called 'vortex' - added new force function to apply a force immediately (and handle the timing in the FX) - added update function to PS for size update to handle dynamic segment size change - made waterfall width dynamic on segment width - removed some debug stuff - added #defines for maximum number of particles/sprays - updated fire parameter to make it look better on ESP8266 - some little bugfixes commit 59f2b9ae98b77b345984c097e79f352ded1abcff Author: Damian Schneider Date: Mon Apr 1 19:04:28 2024 +0200 more bugfixes, some animation tuning, added volcano back in - fixed animation transitions with proper pointer setting in each FX call - added color by age setting to particle system - maybe fixed collision having a tendency to go to the left - fixed bugs in fireworks FX - added fire spread width as a slider - changed max number of particles and sprays based on some ram calculations - some other random fixes commit 136f40ff62fb7f0bf4577cbc769f90a741b17c00 Author: Damian Schneider Date: Sun Mar 31 17:42:48 2024 +0200 More Bugfixes, more converted FX commit 5f824c34b3cfdee2703783c06604f041125386d8 Author: Damian Schneider Date: Sun Mar 31 09:50:46 2024 +0200 many (many!) bugfixes, added fire FX back in (and improved it a lot) added local renderbuffer for huge speed boost -lots of bugfixes in Particle system -added local rendering buffer (renders to buffer in heap) -added fast and accurate color-add function -bugfixes in render function -added improved 'sticky' particles in collision (unfinished business) -added ballpit animation back -lots of tweaks to fire FX and fire rendering functions, looks even better now (still unfinished) -added palette render option to fire still many debug print outputs around, needs cleanup at one point commit 03967a95eaebec20bf1b30bcfdb8346e138ba954 Author: Damian Schneider Date: Fri Mar 29 20:25:13 2024 +0100 put particle system in a class. work in progress. another huge update to the particle system. went through the whole code, rewrote many of the functions. many improvements over the previous code. fixed many bugs (and even an ancient one in rendering function). spent many hours optimizing the code for speed and usability. still a work in progress, debugging is ongoing. more updates to come. commit 162bdaa63d24d75831010b428bc6abc6aec8eb56 Merge: c6d5d3ef 85a51e6c Author: Damian Schneider Date: Sun Mar 24 13:07:57 2024 +0100 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit c6d5d3efa254387702e589df4f413987f106da0f Author: Damian Schneider Date: Sun Mar 24 12:02:55 2024 +0100 updated PS Fireworks with many changes and fine-tuning of parameters -removed spiral explosions -added more versatility to circular explosions -removed user selectable amount of rockets -tuned explosion size of circular explosions to match random explosions (more or less, may need improvement) -changed order of sliders in volcano animation commit 500b84724a470f0140fbd74597eb1a09b958f7d0 Author: Damian Schneider Date: Sat Mar 23 20:11:48 2024 +0100 bugfix commit 35c21572fdf3b1f2e2aa2fc699e22e64f418f6d9 Author: Damian Schneider Date: Sat Mar 23 20:11:23 2024 +0100 Big update: lots of little fixes and big speed boost on fire animation -fixed fire burning more on the left side -fixed crash in particle attractor -added many improvements for ESP8266 -improved particle rendering efficiency -efficiency improvements in general -changed the way fire is rendered, now more than 2x faster -re-tuned fire to new rendering, also seperately tuned it for ESP8266 -changed all random8() to random16() as it runs faster on ESPs -some reformating -some renaming of effect stuff -fine tuning on falling particle effect -improvements to collision handling (faster and better) -added a (temporary) function for speed tests, will be removed again commit 306a850a11c3e3272d2455e3ba2c166dc77e889a Author: Damian Schneider Date: Thu Mar 21 22:42:45 2024 +0100 slight speed improvements in fire, like 1-2FPS commit 913e91025a7d8dcd05496bef0ea34398ffd146f6 Author: Damian Schneider Date: Thu Mar 21 15:55:40 2024 +0100 Particle FX Rename, default parameter tuning, bugfix -Now shorter names, 'PS' in front to filter the list -Tuned default parameters to make them look better by default -Bugfix in particle system (removed duplicate application of velocity) -reduced PS fire RAM usage (less particles, less base flames, no noticeable difference) -some variable renaming commit 7026159da3e87c73952d129a4c923c7f109f862d Author: Damian Schneider Date: Tue Mar 19 20:17:13 2024 +0100 Cleanup & Bugfixes plus major improvements for ESP8266 -added particle reductions for ESP8266 for all FX -Removed collisions from Particle Perlin Noise FX, slows things down and does not contribute to a better effect experience -lots of optimizations for ESP8266, all FX now work (at least on 160MHz but still slow) -Some bugfixes -removed unused variables to make compiler happy commit ff9d2ebd44544e0883f4c765cc10955bf2b30db5 Author: Damian Schneider Date: Sun Mar 17 21:59:42 2024 +0100 FX update - changed firework exhaust to low saturation - updated rotating particle spray animation commit a56d888f8d7c3e7253fb1427dff9ad12039410f0 Author: Damian Schneider Date: Wed Mar 20 19:39:26 2024 +0100 added rotating GEQ, work in progress -animation works but sliders are too sensitive. need to adjust the ranges commit 79917762291bcde032a01d0fea3c738e76acd483 Author: Damian Schneider Date: Sun Mar 17 15:41:46 2024 +0100 GEQ FX parameter tuning commit ecc64ae03dcf7f619218569145b169eea37d00a8 Author: Damian Schneider Date: Sat Mar 16 11:43:22 2024 +0100 Particle GEQ fixes, it now actually works commit 87adbedc71388d0dc035cee4f59a2a7f863ee25e Author: Damian Schneider Date: Fri Mar 15 20:52:47 2024 +0100 added particle GEQ effect (untested) commit bdfb1a9d22a446764bf0619efb669c3cec735767 Merge: 3678aa9d 7b366d49 Author: Damian Schneider Date: Tue Mar 12 21:48:34 2024 +0100 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 3678aa9d9f1b32a099b9fe7503021a9e31d27b69 Merge: ac092181 88b30e7e Author: Damian Schneider Date: Tue Mar 12 21:47:28 2024 +0100 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit ac0921818542e74f5ded49f56d31df9299aaf834 Author: Damian Schneider Date: Tue Mar 12 20:45:52 2024 +0100 cleanup removed / added comments commit 4930cda17d8518b2c3ac0669888820d4a04f4a8a Author: Damian Schneider Date: Tue Mar 12 20:17:02 2024 +0100 cleanup session -removed particle box 'rocking boat' (buggy) and replaced with random sloshing. -removed comments -changed some variables into 32bit for speed boost on ESP32 -added link to KB at the top of FX.cpp commit 9c6d6f19bea04d274b9bc1a7ecf39f2401cac1cf Author: Damian Schneider Date: Sun Mar 10 22:35:13 2024 +0100 cleanup -removed wrap_update function, now integrated into move_update -added more 'out of bounds' checking in fire functions, may speed it up a little commit b99a62feff45e4769ba579ceb0f7474b299dfeb3 Author: Damian Schneider Date: Fri Mar 8 19:23:52 2024 +0100 removed comments, added comments commit 66ac5ac81b6ee25a39482bbe31e9199e8e51ec5a Author: Damian Schneider Date: Fri Mar 8 18:14:59 2024 +0100 Revert "fixed touch buttons for ESP32 S2 and S3" This reverts commit 09041551862361cd8c7444ca25403f5f96a9077b. commit 62a975d687841189e4ef24b5b154f3a3a7fc6779 Author: Damian Schneider Date: Fri Mar 8 18:14:52 2024 +0100 Revert "some tuning for touch buttons on S2/S3" This reverts commit d21ad8e7d1651a96f879d43ef1146a72a6ed8271. commit d01a1511cb4f9d2400541e2e8e0d0a8f20cfbe8c Author: Damian Schneider Date: Mon Feb 26 18:20:07 2024 +0100 add todo commit 6740cb69bc61caabdad40451b47744738aced706 Author: Damian Schneider Date: Thu Feb 22 19:16:42 2024 +0100 chaned rotating spray default parameters commit 1a7ef9b0ca7063abd42846b3346d54e5947302d9 Author: Damian Schneider Date: Wed Feb 21 20:26:17 2024 +0100 updated rotating particle spray with more user options commit d21ad8e7d1651a96f879d43ef1146a72a6ed8271 Author: Damian Schneider Date: Wed Feb 21 18:38:34 2024 +0100 some tuning for touch buttons on S2/S3 now better fits the default threshold value of 32 commit 09041551862361cd8c7444ca25403f5f96a9077b Author: Damian Schneider Date: Sun Feb 18 15:52:36 2024 +0100 fixed touch buttons for ESP32 S2 and S3 touch is implemented differently on S2 and S3, these changes make touch buttons work on S2 and S3 commit a7759702a761eb2158e2d6a08bb31c94cf3890fd Merge: 241b0808 21173dc9 Author: Damian Schneider Date: Sun Feb 18 11:29:00 2024 +0100 Merge remote-tracking branch 'upstream/0_15' into FXparticleSystem commit 241b08082e98942fae2290658ff02eb3c4247828 Author: Damian Schneider Date: Sat Feb 17 16:50:16 2024 +0100 added particle WrapUpdate function, added spray FX, some bugfixes commit 11a84c1b2a142d3debaaeb35335079454830a2ec Author: Damian Schneider Date: Sat Feb 17 14:23:05 2024 +0100 removed TODOs commit 74ed705b9cb465ba573b6bce35ecaabfd64eeb0d Author: Damian Schneider Date: Sat Feb 17 14:19:56 2024 +0100 updated particle attractor animation parameters and added more user options to perlin noise commit 32979e59010831b5011f398dff10777c04c82f2c Author: Damian Schneider Date: Sat Feb 17 12:50:20 2024 +0100 lots of bugfixes commit b96ad99e0a99c393e78b672659577a58b9cbe1d7 Author: Damian Schneider Date: Tue Feb 13 17:00:42 2024 +0100 changed particle pile demo into waterfall plus some tweaks commit 46aef896b252568c3bd2ea2a40a4d6e89fdbef51 Author: Damian Schneider Date: Tue Feb 13 06:47:35 2024 +0100 Bugfix in particle push, now piling is working again particle pile-up did not work correctly, now fixed commit dc5c58e98a0d8ec5dd753b8d8c3c0f9f444d90db Author: Damian Schneider Date: Sat Feb 10 18:25:37 2024 +0100 Fixed some bugs in particle system, runs much smoother now also tweaked some of the FX commit 7c49f886419c0232118c6abc4dcdc70309f94bf9 Author: Damian Schneider Date: Sat Feb 10 13:32:23 2024 +0100 removed option for fastcolor add it made very little difference in performance, but for ESP8266 it may matter so it is set permanently there. graphically the difference is also very small (sometimes a particle gets brighter or less saturated) commit da94d31990a4a7284ed2c7c99fc7eb98023c27b6 Author: Damian Schneider Date: Sat Feb 10 11:32:07 2024 +0100 Improved collision handling (faster, less oscillations), changed variables to 32bit for faster calculation 32bit variables are faster on ESP32, so use them whenever a variable is used a lot, it saves one instruction per access. commit 8fe044ee69161b4854cb71a69129bc07155ada3e Author: Damian Schneider Date: Sat Feb 10 08:14:34 2024 +0100 added fix for piling oscillations untested, need to verify it works commit f1ffbe0cf77866df42482806f85305a15d4ccefd Author: Damian Schneider Date: Sat Feb 10 07:35:45 2024 +0100 improved collision efficiency improved efficiency for stackup (pushback), added code to correctly determine direction if particles meed (probably overkill but now its there) commit e945faf86e578a26c14130eec94dc43e170eb8ac Author: Damian Schneider Date: Fri Feb 9 20:30:26 2024 +0100 collision detection is now a function plus some improvements & fixes commit d00126bce066521cfeb62be7ef3868db520d4250 Author: Damian Schneider Date: Thu Feb 8 22:34:36 2024 +0100 added option to use fast color add, may improve performance a little also fixed a bug in fire animation commit 7d6965d14c3fcfcc6737ae98cf87bed1c1cfe195 Author: Damian Schneider Date: Thu Feb 8 21:50:51 2024 +0100 bugfixes in impact animation commit 18c79cedda9a3eaa20b01fd209b2dd3680203c14 Author: Damian Schneider Date: Thu Feb 8 21:19:30 2024 +0100 Added saturation to be set for each particle individually at the expense of more ram usage, animations now have more options for color control (already used in fireworks now) commit a147a4bd97483aaf08b2197f2b740b686a6f8506 Author: Damian Schneider Date: Thu Feb 8 20:40:21 2024 +0100 added angle emitter, added fireworks effect using it commit 7bc59c6622997013232f9bb6a738db2d64c7aafe Merge: 7bcfcb44 6dcd9596 Author: Damian Schneider Date: Thu Feb 8 18:34:16 2024 +0100 Merge branch '0_15' into FXparticleSystem commit 7bcfcb445af6b617229c94d7fd058521ea472837 Author: Damian Schneider Date: Thu Feb 8 18:33:00 2024 +0100 bugfixes, attracot now works still unknown, why more than 256 particles are needed in memory allocation to not make it crash, but it works for now commit cc98036e3f096d1da0c59e5cce3d1ff96c990fe9 Author: Damian Schneider Date: Wed Feb 7 19:48:54 2024 +0100 added particle attractor, added two new FX but still buggy particle attractor animation does not work, somthing wrong with pointer allocation, it worked with static variables commit 820d8dd3dca5f1acb6111bb38c8092640256bff2 Author: Damian Schneider Date: Tue Feb 6 12:44:48 2024 +0100 added preliminary functions and FX commit 3b8221935c593d0179c5ad629939250835bd6280 Merge: a7ef020c 00038453 Author: Damian Schneider Date: Sun Feb 4 12:46:26 2024 +0100 Merge branch '0_15' into FXparticleSystem commit a7ef020cc96898d5e858895e39662e63f9c8b948 Author: Damian Schneider Date: Sun Feb 4 12:40:37 2024 +0100 updated particle box and firework effects particle box now is more random in random mode (still a work in progress) firework is now more configurable by sliders commit 520a6d54c618b4c22acc8d2084010b6558ac01ec Author: Damian Schneider Date: Sun Feb 4 10:39:01 2024 +0100 Update platformio.ini commit 6165083f160095e73410bcc3aedce7513cb3b094 Author: Damian Schneider Date: Sun Feb 4 10:20:42 2024 +0100 added latest version of functions this somehow also got lost from an earlier commit commit c42e759c5d8ccc7a1b793b5d9f6de9a793d1ffdd Author: Damian Schneider Date: Sun Feb 4 10:17:08 2024 +0100 reformat commit 3e8d0790766790f32b40748026c846960739ffa6 Author: Damian Schneider Date: Sun Feb 4 10:15:58 2024 +0100 added collision handling back in update from another commit that got lost commit c320b0ad80edb72ab09adcc22569f398f9015d86 Author: Damian Schneider Date: Sun Feb 4 10:13:54 2024 +0100 cleanup commit 2db3123d9c6e8f85c19cb6736c3a5fb2d4364988 Author: Damian Schneider Date: Sun Feb 4 10:04:04 2024 +0100 removed comments commit 45f26169043209d7f9cf7dfdc6bb142fa7ba21bd Author: Damian Schneider Date: Sun Feb 4 10:02:46 2024 +0100 added particle FX animations not yet final version but working commit 538ffdbf46abf42e75c261d1a021d0ecf1426543 Author: Damian Schneider Date: Sat Feb 3 10:45:34 2024 +0100 Adding Particle System functions Basic Particle system defining particle structs, some emitters and particle to LED rendering --- CHANGELOG.md | 10 + usermods/audioreactive/audio_reactive.h | 9 +- wled00/FX.cpp | 313 +++++----- wled00/FX.h | 3 +- wled00/FXparticleSystem.cpp | 760 +++++++++++------------- wled00/FXparticleSystem.h | 58 +- wled00/data/index.js | 21 +- wled00/fcn_declare.h | 14 +- wled00/ir.cpp | 254 ++++---- wled00/json.cpp | 2 + wled00/mqtt.cpp | 23 +- wled00/overlay.cpp | 14 +- wled00/playlist.cpp | 3 +- wled00/set.cpp | 7 +- wled00/wled.cpp | 7 + wled00/wled.h | 3 +- 16 files changed, 697 insertions(+), 804 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c58dfa33..e37b08b696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ ## WLED changelog +#### Build 240503 +- Using brightness in analog clock overlay (#3944 by @paspiz85) +- Add Webpage shortcuts (#3945 by @w00000dy) +- ArtNet Poll reply (#3892 by @askask) +- Improved brightness change via long button presses (#3933 by @gaaat98) +- Relay open drain output (#3920 by @Suxsem) +- NEW JSON API: release info (update page, `info.release`) +- update esp32 platform to arduino-esp32 v2.0.9 (#3902) +- various optimisations and bugfixes (#3952, #3922, #3878, #3926, #3919, #3904 @DedeHai) + #### Build 2404120 - v0.15.0-b3 - fix for #3896 & WS2815 current saving diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 61915170c0..442a651eac 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -183,7 +183,6 @@ constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT resul // These are the input and output vectors. Input vectors receive computed results from FFT. static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins static float vImag[samplesFFT] = {0.0f}; // imaginary parts -static float windowWeighingFactors[samplesFFT] = {0.0f}; // Create FFT object // lib_deps += https://github.com/kosme/arduinoFFT#develop @ 1.9.2 @@ -196,7 +195,8 @@ static float windowWeighingFactors[samplesFFT] = {0.0f}; #include -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, windowWeighingFactors); +/* Create FFT object with weighing factor storage */ +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); // Helper functions @@ -1121,6 +1121,11 @@ class AudioReactive : public Usermod { delay(100); // Give that poor microphone some time to setup. useBandPassFilter = false; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + switch (dmType) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) // stub cases for not-yet-supported I2S modes on other ESP32 chips diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c0fc30c747..af9d828744 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7968,8 +7968,7 @@ uint16_t mode_particlevortex(void) } } } - - //TODO: speed increment is still wrong, it only works in autochange, manual change to a lower speed does not work. need to make it better. + // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag @@ -7977,7 +7976,6 @@ uint16_t mode_particlevortex(void) if (SEGMENT.custom2 > 0) // automatic direction change enabled { - // speedincrement = 1 + (SEGMENT.speed >> 5) + (SEGMENT.custom2 >> 2); uint16_t changeinterval = (270 - SEGMENT.custom2); direction = SEGMENT.aux1 & 0x02; //set direction according to flag @@ -8014,22 +8012,6 @@ uint16_t mode_particlevortex(void) // calculate angle offset for an even distribution uint16_t angleoffset = 0xFFFF / spraycount; - - //int32_t particlespeeddiv = ((263 - SEGMENT.intensity) >> 3); - //int32_t particlespeed = 127/particlespeeddiv; //just for testing, need to replace this with angle emit and come up with a new speed calculation - //particle speed goes from 7 to 128 (sin cos return 15bit value but with sign) - - // for (j = 0; j < spraycount; j++) //TODO: use angle emit - // { - - // calculate the x and y speed using aux0 as the 16bit angle. returned value by sin16/cos16 is 16bit, shifting it by 8 bits results in +/-128, divide that by custom1 slider value - // PartSys->sources[j].vx = (cos16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) - // PartSys->sources[j].vy = (sin16(SEGMENT.aux0 + angleoffset * j) >> 8) / particlespeeddiv; // update spray angle (rotate all sprays with angle offset) - // PartSys->sources[j].var = (SEGMENT.custom3 >> 1); // emiting variation = nozzle size (custom 3 goes from 0-32) - // } - - -//test to check if less particles are ok at lower speeds. uint32_t skip = PS_P_HALFRADIUS/(SEGMENT.intensity + 1) + 1; if (SEGMENT.call % skip == 0) { @@ -8072,7 +8054,7 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); @@ -8095,7 +8077,6 @@ uint16_t mode_particlefireworks(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - //PartSys->setWallHardness(SEGMENT.custom2); //not used anymore, can be removed PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time @@ -8136,7 +8117,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed; counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) - angle = random16(); // random start angle + angle = esp_random(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; @@ -8168,6 +8149,7 @@ uint16_t mode_particlefireworks(void) else { /* + //TODO: this does not look good. adjust or remove completely if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { if(i < (emitparticles>>2)) //set 1/4 of particles to larger size @@ -8193,7 +8175,7 @@ uint16_t mode_particlefireworks(void) { if (PartSys->sources[j].source.ttl) { - PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->particlesettings); //todo: need different settings for rocket? + PartSys->particleMoveUpdate(PartSys->sources[j].source); } else if (PartSys->sources[j].source.vy > 0) // rocket has died and is moving up. stop it so it will explode (is handled in the code above) { @@ -8209,16 +8191,16 @@ uint16_t mode_particlefireworks(void) { // reinitialize rocket PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom - PartSys->sources[j].source.x = (rand() % (PartSys->maxX >> 1)) + (PartSys->maxX >> 2); // centered half + PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(5) - 2; //i.e. not perfectly straight up + PartSys->sources[j].source.vx = random(-3,3); //i.e. not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life PartSys->sources[j].minLife = 10; PartSys->sources[j].vx = 0; // emitting speed PartSys->sources[j].vy = 0; // emitting speed - PartSys->sources[j].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[j].var = 3; // speed variation around vx,vy (+/- var/2) } } @@ -8259,7 +8241,7 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[i].source.perpetural = true; // source never dies + PartSys->sources[i].source.perpetual = true; // source never dies } } else @@ -8296,10 +8278,10 @@ uint16_t mode_particlevolcano(void) PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 | 0x01; // emiting variation = nozzle size (custom 3 goes from 0-31), only use odd numbers + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->particlesettings); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) + PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) //Serial.println("emit"); } } @@ -8326,9 +8308,9 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 25, false, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = rand(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = esp_random(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8377,15 +8359,15 @@ uint16_t mode_particlefire(void) // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + (rand() % spread) ; // distribute randomly on chosen width + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good - PartSys->sources[i].var = (random16(2 + (firespeed >> 5)) + 3) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (random16(1 + (firespeed >> 5)) + 2); // speed variation around vx,vy (+/- var) } } @@ -8445,7 +8427,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, 0, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setGravity(); //enable with default gravity @@ -8515,92 +8497,6 @@ uint16_t mode_particlepit(void) PartSys->update(); // update and render -//Experiment: blur to grow the particles, also blur asymmetric, make the particles wobble: - /* - SEGMENT.blur(SEGMENT.custom1, true); - if (SEGMENT.custom1 > 64) - SEGMENT.blur(SEGMENT.custom1 - 64, true); - if (SEGMENT.custom1 > 128) - SEGMENT.blur((SEGMENT.custom1 - 128) << 1, true); - if (SEGMENT.custom1 > 192) - SEGMENT.blur((SEGMENT.custom1 - 192) << 1, true); - */ -/* -//wobbling - static uint8_t testcntr; - static uint8_t wobbleamount = 200; - wobbleamount -= 2; - - testcntr+=15; - -// int32_t ysize = (int16_t)sin8(testcntr); - // int32_t xsize = 255-ysize; - - int32_t ysize = (int32_t)sin8(testcntr)-128; - int32_t xsize = -ysize; //TODO: xsize is not really needed, calculation can be simplified using just ysize - - //ysize = (((int16_t)SEGMENT.custom1 * ysize) >> 8); - //xsize = (((int16_t)SEGMENT.custom1 * xsize) >> 8); - ysize = (int32_t)SEGMENT.custom1 - ((ysize * wobbleamount * SEGMENT.custom1) >> 15); - xsize = (int32_t)SEGMENT.custom1 - ((xsize * wobbleamount * SEGMENT.custom1) >> 15); - - Serial.print(SEGMENT.custom1); - Serial.print(" "); - Serial.print(wobbleamount); - Serial.print(" "); - - Serial.print(xsize); - Serial.print(" "); - Serial.print(ysize); - - - const unsigned cols = PartSys->maxXpixel + 1; - const unsigned rows = PartSys->maxYpixel + 1; - uint8_t xiterations = 1 + (xsize>>8); //allow for wobble size > 255 - uint8_t yiterations = 1 + (ysize>>8); - uint8_t secondpassxsize = xsize - 255; - uint8_t secondpassysize = ysize - 255; - if (xsize > 255) - xsize = 255; //first pass, full sized - if (ysize > 255) - ysize = 255; - - Serial.print(xsize); - Serial.print(" "); - Serial.println(ysize); - for (uint32_t j = 0; j < xiterations; j++) - { - for (uint32_t i = 0; i < cols; i++) - { - SEGMENT.blurCol(i, xsize, true); - if (xsize > 64) - SEGMENT.blurCol(i, xsize - 64, true); - if (xsize > 128) - SEGMENT.blurCol(i, (xsize - 128) << 1, true); - if (xsize > 192) - SEGMENT.blurCol(i, (xsize - 192) << 1, true); - } - //set size for second pass: - xsize = secondpassxsize; - } - for (uint32_t j = 0; j < yiterations; j++) - { - for (unsigned i = 0; i < rows; i++) - { - SEGMENT.blurRow(i, ysize, true); - if (ysize > 64) - SEGMENT.blurRow(i, ysize - 64, true); - if (ysize > 128) - SEGMENT.blurRow(i, (ysize - 128) << 1, true); - if (ysize > 192) - SEGMENT.blurRow(i, (ysize - 192) << 1, true); - } - // set size for second pass: - ysize = secondpassysize; - } -*/ - - /* //rotat image (just a test, non working yet) float angle = PI/3; @@ -8674,7 +8570,6 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].maxLife = 400; // lifetime in frames PartSys->sources[i].minLife = 150; #endif - PartSys->sources[i].var = 7; // emiting variation } } else @@ -8713,7 +8608,7 @@ uint16_t mode_particlewaterfall(void) PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix - PartSys->sources[i].var = (SEGMENT.custom1 >> 3) | 0x01; // emiting variation 0-32, only use odd numbers + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 } for (i = 0; i < numSprays; i++) @@ -8743,10 +8638,28 @@ uint16_t mode_particlebox(void) ParticleSystem *PartSys = NULL; uint32_t i; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1)) // init - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed + PartSys->setBounceX(true); + PartSys->setBounceY(true); + //set max number of particles and save to aux1 for later + #ifdef ESP8266 + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + #else + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); //max number of particles + #endif + for (i = 0; i < SEGMENT.aux1; i++) + { + PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) + PartSys->particles[i].perpetual = true; //never die + PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color + PartSys->particles[i].y = random16(PartSys->maxY >> 2); //bottom quarter + PartSys->particles[i].collide = true; // all particles collide + } + SEGMENT.aux0 = rand(); // position in perlin noise } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8758,31 +8671,11 @@ uint16_t mode_particlebox(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setBounceX(true); - PartSys->setBounceY(true); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - - #ifdef ESP8266 - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); - #else - uint16_t maxnumParticles = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); - #endif - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, maxnumParticles)); - - if (SEGMENT.call == 0) // initialization of particles (cannot be done in above loop, only if code lines above here are copied there) - { - SEGMENT.aux0 = rand(); // position in perlin noise - for (i = 0; i < maxnumParticles; i++) - { - PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].perpetural = true; //never dies - PartSys->particles[i].hue = i * 5; // color range - PartSys->particles[i].x = map(i, 0, maxnumParticles, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY); // randomly in y direction - PartSys->particles[i].collide = true; // all particles collide - } - } + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); //aux1 holds max number of particles to use + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting { @@ -8823,7 +8716,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=43,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -8838,7 +8731,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init with 1 source and advanced properties + if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8987,7 +8880,7 @@ uint16_t mode_particleimpact(void) if (PartSys->sources[i].source.vy < 0) //move down { PartSys->applyGravity(&PartSys->sources[i].source); - PartSys->particleMoveUpdate(PartSys->sources[i].source, meteorsettings); + PartSys->particleMoveUpdate(PartSys->sources[i].source, &meteorsettings); // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down @@ -9006,7 +8899,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.ttl = random16((255 - SEGMENT.speed)) + 10; // standby time til next launch (in frames at 42fps, max of 265 is about 6 seconds #endif PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y - PartSys->sources[i].var = (SEGMENT.custom1 >> 1) | 0x01; // speed variation around vx,vy (+/- var/2), only use odd numbers + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } } } @@ -9023,7 +8916,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].maxLife = 60; // spark particle life PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) - PartSys->sources[i].var = 5; // speed variation around vx,vy (+/- var/2) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } @@ -9051,14 +8944,13 @@ uint16_t mode_particleattractor(void) PSparticle *attractor; //particle pointer to the attractor if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed; //allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.vx = -7; - PartSys->sources[0].source.collide = true; // seeded particles will collide - //PartSys->sources[0].source.ttl = 100; //TODO: remove, is now done in PS init - PartSys->sources[0].source.perpetural = true; //source does not age + PartSys->sources[0].source.vx = -7; //will collied with wall and get random bounce direction + PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.perpetual = true; //source does not age #ifdef ESP8266 PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; @@ -9066,7 +8958,7 @@ uint16_t mode_particleattractor(void) PartSys->sources[0].maxLife = 350; // lifetime in frames PartSys->sources[0].minLife = 50; #endif - PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].var = 4; // emiting variation PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } @@ -9099,13 +8991,13 @@ uint16_t mode_particleattractor(void) attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y attractor->vy = PartSys->sources[0].source.vx; attractor->ttl = 100; - attractor->perpetural = true; + attractor->perpetual = true; } //set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly - PartSys->particleMoveUpdate(*attractor, sourcesettings); // move the attractor + PartSys->particleMoveUpdate(*attractor, &sourcesettings); // move the attractor } else{ @@ -9129,7 +9021,7 @@ uint16_t mode_particleattractor(void) } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); - PartSys->particleMoveUpdate(PartSys->sources[0].source, sourcesettings); // move the source + PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } @@ -9177,7 +9069,7 @@ uint16_t mode_particleattractor(void) #endif PartSys->sources[0].vx = 0; // emitting speed PartSys->sources[0].vy = 0; // emitting speed - PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].var = 4; // emiting variation } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9277,7 +9169,7 @@ uint16_t mode_particlespray(void) PartSys->sources[0].maxLife = 300; // lifetime in frames PartSys->sources[0].minLife = 100; PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) - PartSys->sources[0].var = 7; + PartSys->sources[0].var = 3; } else @@ -9306,10 +9198,9 @@ uint16_t mode_particlespray(void) if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } @@ -9423,7 +9314,7 @@ static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Inte Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ #define MAXANGLESTEP 2200 //32767 means 180° -uint16_t mode_particlghostrider(void) +uint16_t mode_particleghostrider(void) { if (SEGLEN == 1) return mode_static(); @@ -9469,8 +9360,7 @@ uint16_t mode_particlghostrider(void) // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(SEGMENT.custom1); - //PartSys->setColorByAge(SEGMENT.check1); - PartSys->sources[0].var = (1 + (SEGMENT.custom3>>1)) | 0x01; //odd numbers only + PartSys->sources[0].var = SEGMENT.custom3 >> 1; //color by age (PS color by age always starts with hue = 255 so cannot use that) if(SEGMENT.check1) @@ -9492,8 +9382,8 @@ uint16_t mode_particlghostrider(void) int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.ttl = 500; //source never dies - PartSys->particleMoveUpdate(PartSys->sources[0].source, ghostsettings); + PartSys->sources[0].source.ttl = 500; //source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); //set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; @@ -9509,6 +9399,84 @@ uint16_t mode_particlghostrider(void) } static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,Color by age,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1,o2=0,o3=0"; + +/* +PS Blobs: large particles bouncing around +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem *PartSys = NULL; + + if (SEGMENT.call == 0) + { + if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed; //allocation failed + //PartSys->setGravity(); //enable with default gravity + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) //speed changed or dead + { + PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + } + if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) //size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); //set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize + { + PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; //enable collision for particle + PartSys->advPartProps[i].size = 0; //start out small + PartSys->advPartSize[i].asymmetry = random16(220); + PartSys->advPartSize[i].asymdir = random16(255); + //set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + random(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGMENT.aux0 = SEGMENT.speed; //write state back + SEGMENT.aux1 = SEGMENT.custom1; + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; + + /* * Particle rotating GEQ * Particles sprayed from center with a rotating spray @@ -9893,7 +9861,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); - addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particlghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); diff --git a/wled00/FX.h b/wled00/FX.h index 03c15f6825..3d98cb2c27 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -327,7 +327,8 @@ #define FX_MODE_PARTICLESGEQ 198 #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 -#define MODE_COUNT 201 +#define FX_MODE_PARTICLEBLOBS 201 +#define MODE_COUNT 202 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a0eb31252..3a8b4cf7ae 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,7 +33,7 @@ /* TODO: - -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. config should be optional, if not set, use default config. + -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not -add an x/y struct, do particle rendering using that, much easier to read */ @@ -43,14 +43,15 @@ #include "FastLED.h" #include "FX.h" -ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) +ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { //Serial.println("PS Constructor"); numSources = numberofsources; numParticles = numberofparticles; // set number of particles in the array usedParticles = numberofparticles; // use all particles by default - //particlesettings = {false, false, false, false, false, false, false, false}; // all settings off by default - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default @@ -69,35 +70,25 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numbero { particles[i].sat = 255; // full saturation } - /* - Serial.println("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) - { - aliveparticles++; - } - Serial.println(aliveparticles); - for (int i = 0; i < numParticles; i++) - { - //particles[i].ttl = 0; //initialize all particles to dead - //if (particles[i].ttl) - { - Serial.print("x:"); - Serial.print(particles[i].x); - Serial.print(" y:"); - Serial.println(particles[i].y); - } - }*/ - // Serial.println("PS Constructor done"); + //Serial.println("PS Constructor done"); } -//update function applies gravity, moves the particles, handles collisions and renders the particles +// update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { PSadvancedParticle *advprop = NULL; //apply gravity globally if enabled if (particlesettings.useGravity) applyGravity(); + + //update size settings before handling collisions + if(advPartSize) + { + for (int i = 0; i < usedParticles; i++) + { + updateSize(&advPartProps[i], &advPartSize[i]); + } + } // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) if (particlesettings.useCollisions) @@ -108,11 +99,12 @@ void ParticleSystem::update(void) { if(advPartProps) { - advprop = &advPartProps[i]; + advprop = &advPartProps[i]; } - particleMoveUpdate(particles[i], particlesettings, advprop); + particleMoveUpdate(particles[i], &particlesettings, advprop); } - /*!!! remove this + + /*TODO remove this Serial.print("alive particles: "); uint32_t aliveparticles = 0; for (int i = 0; i < numParticles; i++) @@ -125,7 +117,7 @@ void ParticleSystem::update(void) ParticleSys_render(); } -//update function for fire animation +// update function for fire animation void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { if(!renderonly) @@ -158,7 +150,7 @@ void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) maxXpixel = x - 1; // last physical pixel that can be drawn to maxYpixel = y - 1; maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements - maxY = y * PS_P_RADIUS - 1; // this value is often needed by FX to calculate positions + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem::setWrapX(bool enable) @@ -193,7 +185,7 @@ void ParticleSystem::setColorByAge(bool enable) void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) //only allwo motion blurring on default particle size + if(particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) motionBlur = bluramount; } @@ -201,8 +193,8 @@ void ParticleSystem::setMotionBlur(uint8_t bluramount) void ParticleSystem::setParticleSize(uint8_t size) { particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + particlesize; //note: this sets size if not using advanced props - motionBlur = 0; //disable motion blur if particle size is set + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props + motionBlur = 0; // disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() @@ -224,40 +216,30 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -// emit one particle with variation -void ParticleSystem::sprayEmit(PSsource &emitter) +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem::sprayEmit(PSsource &emitter) { - for (uint32_t i = 0; i < usedParticles; i++) + for (int32_t i = 0; i < usedParticles; i++) { emitIndex++; if (emitIndex >= usedParticles) emitIndex = 0; if (particles[emitIndex].ttl == 0) // find a dead particle { - particles[emitIndex].x = emitter.source.x; // + random16(emitter.var) - (emitter.var >> 1); //randomness uses cpu cycles and is almost invisible, removed for now. - particles[emitIndex].y = emitter.source.y; // + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].vx = emitter.vx + random(emitter.var) - (emitter.var>>1); - particles[emitIndex].vy = emitter.vy + random(emitter.var) - (emitter.var>>1); - particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife; + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].sat = emitter.source.sat; particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); if (advPartProps) advPartProps[emitIndex].size = emitter.size; - break; + return i; } - /* - if (emitIndex < 2) - { - Serial.print(" "); - Serial.print(particles[emitIndex].ttl); - Serial.print(" "); - Serial.print(particles[emitIndex].x); - Serial.print(" "); - Serial.print(particles[emitIndex].y); - }*/ } - //Serial.println("**"); + return -1; } // Spray emitter for particles used for flames (particle TTL depends on source TTL) @@ -272,18 +254,22 @@ void ParticleSystem::flameEmit(PSsource &emitter) { particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base particles[emitIndex].y = emitter.source.y; - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); //random16 is good enough for fire and much faster + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].ttl = random16(emitter.maxLife - emitter.minLife) + emitter.minLife + emitter.source.ttl; // flame intensity dies down with emitter TTL + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; // fire uses ttl and not hue for heat, so no need to set the hue break; // done } } + /* + // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment + uint32_t partidx = sprayEmit(emitter); //emit one particle + // adjust properties + particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL + */ } -//todo: idee: man könnte einen emitter machen, wo die anzahl emittierten partikel von seinem alter abhängt. benötigt aber einen counter -//idee2: source einen counter hinzufügen, dann setting für emitstärke, dann müsste man das nicht immer in den FX animationen handeln - // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) @@ -291,39 +277,34 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! sprayEmit(emitter); - /* - Serial.print(" x: "); - Serial.print(emitter.vx); - Serial.print(" y: "); - Serial.println(emitter.vy);*/ } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) { - + if(options == NULL) + options = &particlesettings; //use PS system settings by default if (part.ttl > 0) { - - if(!part.perpetural) + if(!part.perpetual) part.ttl--; // age if (particlesettings.colorByAge) part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - bool usesize = false; //particle uses individual size rendering + bool usesize = false; // particle uses individual size rendering int32_t newX = part.x + (int16_t)part.vx; int32_t newY = part.y + (int16_t)part.vy; part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - if(advancedproperties) //may be using individual particle size + if(advancedproperties) //using individual particle size? { if(advancedproperties->size > 0) - usesize = true; //note: variable eases out of frame checking below + usesize = true; // note: variable eases out of frame checking below particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); } - //if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options.bounceX) + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounceX) { if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall bounce(part.vx, part.vy, newX, maxX); @@ -331,14 +312,14 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { - if (options.wrapX) + if (options->wrapX) { newX = (uint16_t)newX % (maxX + 1); } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { bool isleaving = true; - if(usesize) //using individual particle size + if(usesize) // using individual particle size { if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough isleaving = false; @@ -347,13 +328,13 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) part.ttl = 0; } } } - if (options.bounceY) + if (options->bounceY) { if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling { @@ -361,44 +342,34 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P bounce(part.vy, part.vx, newY, maxY); else { - /* - //TODO: is this check really needed? is checked below. on quick tests, it crashed (but not in all animations... -> seems ok. leave it for now, need to check this later - if (options.useGravity) // do not bounce on top if using gravity (open container) if this is needed implement it in the FX - { - if (newY > maxY + PS_P_HALFRADIUS) - part.outofbounds = 1; // set out of bounds, kill out of bounds over the top does not apply if gravity is used (user can implement it in FX if needed) - } - else*/ - if(!options.useGravity) - { + if(!options->useGravity) bounce(part.vy, part.vx, newY, maxY); - } } } } if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) { - if (options.wrapY) + if (options->wrapY) { newY = (uint16_t)newY % (maxY + 1); } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { bool isleaving = true; - if(usesize) //using individual particle size + if(usesize) // using individual particle size { - if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) //still withing rendering reach + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach isleaving = false; } if(isleaving) { part.outofbounds = 1; - if (options.killoutofbounds) + if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground part.ttl = 0; - else if (!options.useGravity) + else if (!options->useGravity) part.ttl = 0; } } @@ -409,25 +380,107 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings &options, P } } -//function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) -void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) +// update advanced particle size control +void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { - incomingspeed = -incomingspeed; // invert speed - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted - else - position = maxposition - particleHardRadius; - if(wallRoughness) - { - //transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = ((random(donatespeed << 1) - donatespeed) * wallRoughness) / 255; //take random portion of + or - x speed, scaled by roughness - parallelspeed += donatespeed; - donatespeed = abs(donatespeed); - incomingspeed -= incomingspeed > 0 ? donatespeed : -donatespeed; - } + if(advsize == NULL) // just a safety check + return; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if(advsize->grow) increment = advsize->growspeed; + else if(advsize->shrink) increment = advsize->shrinkspeed; + if(increment < 9) // 8 means +1 every frame + { + counter += increment; + if(counter > 7) + { + counter -= 8; + increment = 1; + } + else + increment = 0; + advsize->sizecounter = counter; + } + else{ + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + if(advsize->grow) + { + if(newsize < advsize->maxsize) + { + newsize += increment; + if(newsize >= advsize->maxsize) + { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if(advsize->pulsate) advsize->shrink = true; + } + } + } + else if(advsize->shrink) + { + if(newsize > advsize->minsize) + { + newsize -= increment; + if(newsize <= advsize->minsize) + { + //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if(advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if(advsize->wobble) + { + advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... + } +} +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) +{ + if(advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + return; + int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (advsize->asymdir < 64) { + deviation = ((int32_t)advsize->asymdir * deviation) / 64; + } else if (advsize->asymdir < 192) { + deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; + } else { + deviation = (((int32_t)advsize->asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle lareger sizes) + xsize = ((int32_t)advprops->size - deviation) > 255 ? 255 : advprops->size - deviation; + ysize = ((int32_t)advprops->size + deviation) > 255 ? 255 : advprops->size + deviation; +} + +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) +{ + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if(wallRoughness) + { + int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); + donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } } // apply a force in x,y direction to individual particle @@ -461,12 +514,12 @@ void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yf } // apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { // for small forces, need to use a delay counter uint8_t tempcounter; - - //note: this is not the most compuatationally effeicient way to do this, but it saves on duplacte code and is fast enough + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough for (uint i = 0; i < usedParticles; i++) { tempcounter = forcecounter; @@ -493,6 +546,7 @@ void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint1 return; // no advanced properties available applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); } + // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) @@ -502,9 +556,9 @@ void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) applyForce(xforce, yforce); } -// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) // apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem::applyGravity() { @@ -516,8 +570,8 @@ void ParticleSystem::applyGravity() } } -//apply gravity to single particle using system settings (use this for sources) -//function does not increment gravity counter, if gravity setting is disabled, this cannot be used +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { int32_t dv; // velocity increase @@ -533,7 +587,8 @@ void ParticleSystem::applyGravity(PSparticle *part) } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) -void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) { int32_t friction = 255 - coefficient; // note: not checking if particle is dead can be done by caller (or can be omitted) @@ -543,7 +598,7 @@ void ParticleSystem::applyFriction(PSparticle *part, uint8_t coefficient) } // apply friction to all particles -void ParticleSystem::applyFriction(uint8_t coefficient) +void ParticleSystem::applyFriction(int32_t coefficient) { for (uint32_t i = 0; i < usedParticles; i++) { @@ -586,17 +641,18 @@ void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attracto applyForce(particleindex, xforce, yforce); } +/* //attract to a line (TODO: this is not yet working) void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { // Calculate the distance between the particle and the attractor if(advPartProps == NULL) - return; //no advanced properties available + return; // no advanced properties available - //calculate a second point on the line + // calculate a second point on the line int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); - //calculate squared distance from particle to the line: + // calculate squared distance from particle to the line: int32_t dx = (x1 - attractorcenter->x) >> 4; int32_t dy = (y1 - attractorcenter->y) >> 4; int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; @@ -614,7 +670,7 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor //apply force in a 90° angle to the line int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! -/* + Serial.print(" partx: "); Serial.print(particles[particleindex].x); Serial.print(" party "); @@ -636,10 +692,11 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor Serial.print(" fx: "); Serial.print(xforce); Serial.print(" fy: "); - Serial.println(yforce);*/ + Serial.println(yforce); applyForce(particleindex, xforce, yforce); -} +}*/ + // render particles to the LED buffer (uses palette to render the 8bit particle color value) // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds @@ -655,18 +712,19 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t brightness; // particle brightness, fades if dying if (useLocalBuffer) - { - cli(); //no interrupts so we get one block of memory for both buffers, less fragmentation + { /* + //memory fragmentation check: Serial.print("heap: "); Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));*/ - // allocate empty memory for the local renderbuffer + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + // allocate empty memory for the local renderbuffer framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - sei(); //re enable interrupts Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } @@ -675,7 +733,6 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it } - sei(); //re enable interrupts if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; @@ -711,6 +768,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) // generate RGB values for particle if(firemode) { + //TODO: decide on a final version... //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed @@ -725,9 +783,9 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); if (particles[i].sat < 255) { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to hsv - baseHSV.s = particles[i].sat; - baseRGB = (CRGB)baseHSV; //convert back to RGB + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = particles[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB } } @@ -736,31 +794,24 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if(particlesize > 0) { - if (useLocalBuffer) + uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + + for(int i = 0; i < passes; i++) { - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); - } - else - { - SEGMENT.blur(particlesize, true); - if (particlesize > 64) - SEGMENT.blur(particlesize - 64, true); - if (particlesize > 128) - SEGMENT.blur((particlesize - 128) << 1, true); - if (particlesize > 192) - SEGMENT.blur((particlesize - 192) << 1, true); - } - + if(i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (useLocalBuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else + SEGMENT.blur(bluramount << bitshift, true); + bluramount -= 64; + } } - if (useLocalBuffer) //transfer local buffer back to segment + if (useLocalBuffer) // transfer local buffer back to segment { uint32_t yflipped; for (int y = 0; y <= maxYpixel; y++) @@ -771,18 +822,18 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } } - free(framebuffer); // free buffer memory + free(framebuffer); } if(renderbuffer) - free(renderbuffer); // free buffer memory + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { - int32_t pxlbrightness[4] = {0}; //note: pxlbrightness needs to be set to 0 or checking does not work - int32_t pixco[4][2]; //physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs - bool advancedrender = false; //rendering for advanced particles + int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; // rendering for advanced particles // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; @@ -792,7 +843,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) int32_t y = yoffset >> PS_P_RADIUS_SHIFT; - //check if particle has advanced size properties and buffer is available + // check if particle has advanced size properties and buffer is available if(advPartProps) { if(advPartProps[particleindex].size > 0) @@ -800,26 +851,25 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if(renderbuffer) { advancedrender = true; - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); //clear the buffer, renderbuffer is 10x10 pixels + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else - return; //cannot render without buffer, advanced size particles are allowed out of frame + return; // cannot render without buffer, advanced size particles are allowed out of frame } } // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixco[0][0] = pixco[3][0] = x; // bottom left & top left - pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right - pixco[2][1] = pixco[3][1] = y + 1; // top right & top left + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[3][1] = y + 1; // top right & top left // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) - if (x < 0) // left pixels out of frame { dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) - //note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) - //checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: + // note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) + // checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: if (dx == PS_P_RADIUS) { pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render @@ -857,7 +907,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, pxlbrightness[2] = pxlbrightness[3] = -1; } - if(advancedrender) //always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + if(advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) { for(uint32_t i = 0; i < 4; i++) pxlbrightness[i] = 0; @@ -883,41 +933,42 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { //render particle to a bigger size //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 - - //first, render the pixel to the renderbuffer, then apply 2D blurring + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 4; - uint32_t offset = 3; //offset to zero coordinate to write/read data in renderbuffer - //TODO: add asymmetrical size support - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size, advPartProps[particleindex].size, true, offset, offset, true); //blur to 4x4 - if (advPartProps[particleindex].size > 64) + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if(advPartSize) // use advanced size control { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, advPartProps[particleindex].size - 64, advPartProps[particleindex].size - 64, true, offset, offset, true); //blur to 6x6 - } - if (advPartProps[particleindex].size > 128) - { - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 128) << 1, (advPartProps[particleindex].size - 128) << 1, true, offset, offset, true); //blur to 8x8 + if(advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = xsize; + if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two } - if (advPartProps[particleindex].size > 192) + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for(int i = 0; i < maxsize; i++) { + if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, (advPartProps[particleindex].size - 192) << 1, (advPartProps[particleindex].size - 192) << 1, true, offset, offset, true); //blur to 10x10 - } + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } - //calculate origin coordinates to render the particle to in the framebuffer + // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; //coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - //transfer renderbuffer to framebuffer + // transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { xfb = xfb_orig + xrb; @@ -939,13 +990,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, else continue; } - - //if(xfb < maxXpixel +1 && yfb < maxYpixel +1) fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); } } } - else + else // standard rendering { if (framebuffer) { @@ -965,45 +1014,6 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } } - //TODO: for advance pixels, render them to larger size in a local buffer. or better make a new function for that? - //easiest would be to create a 2x2 buffer for the original values, but that may not be as fast for smaller pixels... - //just make a new function that these colors are rendered to and then blurr it so it stays fast for normal rendering. - -/* - //uint32_t firstblur = particlesize > 64 ? 64 : particlesize; //attempt to add motion blurring, but does not work... - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize, particlesize); //4x4 - if (particlesize > 64) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, particlesize - 64, particlesize - 64); //6x6 - if (particlesize > 128) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 128) << 1, (particlesize - 128) << 1); 8x8 - if (particlesize > 192) - blur2D(framebuffer, maxYpixel + 1, maxYpixel + 1, (particlesize - 192) << 1, (particlesize - 192) << 1); 10x10 - */ - - -/* - Serial.print("x:"); - Serial.print(particle->x); - Serial.print(" y:"); - Serial.print(particle->y); - //Serial.print(" xo"); - //Serial.print(xoffset); - //Serial.print(" dx"); - //Serial.print(dx); - //Serial.print(" "); - for(uint8_t t = 0; t<4; t++) - { - Serial.print(" v"); - Serial.print(pxlbrightness[t]); - Serial.print(" x"); - Serial.print(pixco[t][0]); - Serial.print(" y"); - Serial.print(pixco[t][1]); - - Serial.print(" "); - } - Serial.println(" "); -*/ /* // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) @@ -1097,7 +1107,7 @@ void ParticleSystem::handleCollisions() uint32_t startparticle = 0; uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) - // if m ore accurate collisions are needed, just call it twice in a row + // if more accurate collisions are needed, just call it twice in a row if (collisioncounter & 0x01) { startparticle = endparticle; @@ -1105,23 +1115,20 @@ void ParticleSystem::handleCollisions() } collisioncounter++; - //startparticle = 0;//!!!TODO test: do all collisions every frame - //endparticle = usedParticles; - for (i = startparticle; i < endparticle; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view { int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) - { // check against higher number particles + for (j = i + 1; j < usedParticles; j++) // check against higher number particles + { if (particles[j].ttl > 0) // if target particle is alive { dx = particles[i].x - particles[j].x; if(advPartProps) //may be using individual particle size { - particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); //collision distance + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance } if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction { @@ -1135,11 +1142,9 @@ void ParticleSystem::handleCollisions() } } - - // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) //TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles +void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? { int32_t dx = particle2->x - particle1->x; int32_t dy = particle2->y - particle1->y; @@ -1167,10 +1172,9 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl distanceSquared = 2; //1 + 1 } - // Calculate dot product of relative velocity and relative distance - - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); //is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; // random16(2); //dotprouct LSB should be somewhat random, so no need to calculate a random number + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number if (dotProduct < 0) // particles are moving towards each other { @@ -1180,14 +1184,14 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // Calculate new velocities after collision uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - int32_t ximpulse = ((impulse) * dx) / 32767; //cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; particle1->vx += ximpulse; particle1->vy += yimpulse; particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; @@ -1205,145 +1209,46 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; } } - - // this part is for particle piling: slow them down if they are close (they become sticky) and push them so they counteract gravity + // particles have volume, push particles apart if they are too close // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { - /** - //only apply friction if particles are slow or else fast moving particles (as in explosions) get slowed a lot - relativeVy *= relativeVy; //square the speed, apply friction if speed is below 10 - if (relativeVy < 100) //particles are slow in y direction -> this works but most animations look much nicer without this friction. add friction in FX if required. - { - //now check x as well (no need to check if y speed is high, this saves some computation time) - relativeVx *= relativeVx; // square the speed, apply friction if speed is below 10 - if (relativeVx < 100) // particles are slow in x direction - { - particle1->vx = ((int32_t)particle1->vx * 254) / 256; - particle2->vx = ((int32_t)particle2->vx * 254) / 256; - - particle1->vy = ((int32_t)particle1->vy * 254) / 256; - particle2->vy = ((int32_t)particle2->vy * 254) / 256; - - } - }*/ - - - - // const int32_t HARDDIAMETER = 2 * particleHardRadius; // push beyond the hard radius, helps with keeping stuff fluffed up -> not really - // int32_t push = (2 * particleHardRadius * particleHardRadius - distanceSquared) >> 6; // push a small amount, if pushing too much, it becomse chaotic as waves of pushing run through piles + { int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are - int32_t push; - - // if (dx < HARDDIAMETER && dx > -HARDDIAMETER) //this is always true as it is checked before ntering this function! - { // distance is too small, push them apart - push = 0; - if (dx < 0) // particle 1 is on the right - push = pushamount; //(HARDDIAMETER + dx) / 4; - else if (dx > 0) - push = -pushamount; //-(HARDDIAMETER - dx) / 4; - else // on the same x coordinate, shift it a little so they do not stack - { - - if (notsorandom) - particle1->x++; // move it so pile collapses - else - particle1->x--; - } - - particle1->vx += push; - } - - // if (dy < HARDDIAMETER && dy > -HARDDIAMETER) //dito + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else // on the same x coordinate, shift it a little so they do not stack { - push = 0; - if (dy < 0) - push = pushamount; //(HARDDIAMETER + dy) / 4; - else if (dy > 0) - push = -pushamount; //-(HARDDIAMETER - dy) / 4; - else // dy==0 - { - if (notsorandom) - particle1->y++; // move it so pile collapses - else - particle1->y--; - } - - particle1->vy += push; - } - /* - if (dx < HARDDIAMETER && dx > -HARDDIAMETER) - { // distance is too small, push them apart - push = 0; - if (dx < 0) // particle 1 is on the right - push = 2; //(HARDDIAMETER + dx) / 4; - else if (dx > 0) - push = -2; //-(HARDDIAMETER - dx) / 4; - else //on the same x coordinate, shift it a little so they do not stack - particle1->x += 2; - if (notsorandom) // chose one of the particles to push, avoids oscillations - { - if (!particle1->flag3) - { - particle1->vx += push; - particle1->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle1->flag3 = 0; //reset - } + if (notsorandom) + particle1->x++; // move it so pile collapses else - { - if (!particle2->flag3) - { - particle2->vx -= push; - particle2->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle2->flag3 = 0; // reset - } + particle1->x--; } - - if (dy < HARDDIAMETER && dy > -HARDDIAMETER) + particle1->vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else // dy==0 { - push = 0; - if (dy < 0) - push = 2; //(HARDDIAMETER + dy) / 4; - else if (dy > 0) - push = -2; //-(HARDDIAMETER - dy) / 4; - - if (!notsorandom) // chose one of the particles to push, avoids oscillations - { - if (!particle1->flag3) - { - particle1->vy += push; - particle1->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle1->flag3 = 0; // reset - } + if (notsorandom) + particle1->y++; // move it so pile collapses else - { - if (!particle2->flag3) - { - particle2->vy -= push; - particle2->flag3 = 1; // particle was pushed, is reset on next push request - } - else - particle2->flag3 = 0; // reset - } - }*/ - + particle1->y--; + } + particle1->vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye } } - - } -//calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) -//force is in 3.4 fixedpoint notation, +/-127 +// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +// force is in 3.4 fixedpoint notation, +/-127 int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) { if(force == 0) @@ -1368,7 +1273,7 @@ int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) return dv; } -//limit speed to prevent overflows +// limit speed to prevent overflows int32_t ParticleSystem::limitSpeed(int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); @@ -1387,55 +1292,60 @@ CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) for (uint i = 0; i < cols; i++) { array2D[i] = start + i * rows; - } - //memset(start, 0, cols * rows * sizeof(CRGB)); // set all values to zero (TODO: remove, not needed if calloc is used) + } } return array2D; } -//update size and pointers (memory location and size can change dynamically) -//note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) void ParticleSystem::updateSystem(void) { // update matrix size uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) // function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) // FX handles the PSsources, need to tell this function how many there are -void ParticleSystem::updatePSpointers(bool isadvanced) +void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { - //DEBUG_PRINT(F("*** PS pointers ***")); - //DEBUG_PRINTF_P(PSTR("this PS %p "), this); - //Note on memory alignment: - //a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. - //The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. - //by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) if(isadvanced) { - advPartProps = reinterpret_cast(particles + numParticles); - sources = reinterpret_cast(advPartProps + numParticles); // pointer to source(s) + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if(sizecontrol) + { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } } else { - advPartProps = NULL; - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - } - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - - //DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - //DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - //DEBUG_PRINTF_P(PSTR(" adv. props %p\n"), advPartProps); - //DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + } + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ } //non class functions to use for initialization -uint32_t calculateNumberOfParticles(bool isadvanced) +uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1452,7 +1362,10 @@ uint32_t calculateNumberOfParticles(bool isadvanced) numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - //make sure it is a multiple of 4 for proper memory alignment + if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles /= 8; // if size control is used, much fewer particles are needed + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = ((numberofParticles+3) >> 2) << 2; return numberofParticles; } @@ -1471,19 +1384,21 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif - //make sure it is a multiple of 4 for proper memory alignment + // make sure it is a multiple of 4 for proper memory alignment numberofSources = ((numberofSources+3) >> 2) << 2; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); - //functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; requiredmemory += additionalbytes; //Serial.print("allocating: "); @@ -1495,14 +1410,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool isadvanced, uint16_t additionalbytes) +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(isadvanced); + uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); uint32_t numsources = calculateNumberOfSources(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, isadvanced, additionalbytes)) + if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1512,7 +1427,7 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, isadvanced); // particle system constructor TODO: why does VS studio thinkt this is bad? + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; @@ -1522,7 +1437,6 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool // Utility Functions // /////////////////////// - // fastled color adding is very inaccurate in color preservation // a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow // this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) @@ -1542,13 +1456,13 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) b = c1.b + c2.b; } uint32_t max = r; - if (g > max) //note: using ? operator would be slower by 2 instructions + if (g > max) // note: using ? operator would be slower by 2 instructions max = g; if (b > max) max = b; if (max < 256) { - c1.r = r; //save result to c1 + c1.r = r; // save result to c1 c1.g = g; c1.b = b; } @@ -1560,7 +1474,7 @@ void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) } } -//faster than fastled color scaling as it uses a 32bit scale factor and pointer +// faster than fastled color scaling as it uses a 32bit scale factor and pointer void fast_color_scale(CRGB &c, uint32_t scale) { c.r = ((c.r * scale) >> 8); @@ -1568,15 +1482,11 @@ void fast_color_scale(CRGB &c, uint32_t scale) c.b = ((c.b * scale) >> 8); } - - // blur a matrix in x and y direction, blur can be asymmetric in x and y // for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) { - - //TODO: for particle rendering, first row and last row can be skipped in x blurring as it is all black, this would increase rendering speed CRGB seeppart, carryover; uint32_t seep = xblur >> 1; if(isparticle) //first and last row are always black in particle rendering @@ -1589,22 +1499,22 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, carryover = BLACK; for(uint32_t x = xstart; x < xstart + xsize; x++) { - seeppart = colorbuffer[x][y]; //create copy of current color - fast_color_scale(seeppart, seep); //scale it and seep to neighbours - if(!smear) //fade current pixel if smear is disabled + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if(!smear) // fade current pixel if smear is disabled fast_color_scale(colorbuffer[x][y], 255 - xblur); if(x > 0) { fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; } fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel } - if(isparticle) //now also do first and last row + if(isparticle) // now also do first and last row { ystart--; ysize++; @@ -1616,15 +1526,15 @@ void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, carryover = BLACK; for(uint32_t y = ystart; y < ystart + ysize; y++) { - seeppart = colorbuffer[x][y]; //create copy of current color - fast_color_scale(seeppart, seep); //scale it and seep to neighbours - if(!smear) //fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if(!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - yblur); if(y > 0) { fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); //todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster } carryover = seeppart; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 7413dccedc..281afee228 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -58,7 +58,7 @@ typedef struct { uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area bool collide : 1; //if set, particle takes part in collisions - bool perpetural : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool perpetual : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) bool flag4 : 1; // unused flag } PSparticle; @@ -67,29 +67,24 @@ typedef struct { uint8_t size; //particle size, 255 means 10 pixels in diameter uint8_t forcecounter; //counter for applying forces to individual particles - - //bool flag1 : 1; // unused flags... for now. - //bool flag2 : 1; - //bool flag3 : 1; - //bool flag4 : 1; } PSadvancedParticle; -// struct for advanced particle size control (optional) TODO: this is currently just an idea, may not make it into final code if too slow / complex +// struct for advanced particle size control (optional) typedef struct { - uint8_t sizeasymmetry; // asymmetrical size TODO: need something better to define this? - uint8_t targetsize; // target size for growing / shrinking + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; uint8_t growspeed : 4; - uint8_t shrinkspeed : 4; - uint8_t sizecounter; // counter that can be used for size contol TODO: need more than one? - //ideas: - //wobbleamount, rotation angle for asymmetic particles - //a flag 'usegravity' that can be set to false for selective gravity application - + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; - bool wobble : 1; - bool flag4 : 1; + bool pulsate : 1; //grows & shrinks & grows & ... + bool wobble : 1; //alternate x and y size } PSsizeControl; @@ -125,19 +120,19 @@ typedef union class ParticleSystem { public: - ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor + ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false, bool sizecontrol = false); // constructor // note: memory is allcated in the FX function, no deconstructor needed void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters + int32_t sprayEmit(PSsource &emitter); void flameEmit(PSsource &emitter); - void sprayEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); // move functions - void particleMoveUpdate(PSparticle &part, PSsettings &options, PSadvancedParticle *advancedproperties = NULL); + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -146,8 +141,8 @@ class ParticleSystem void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles - void applyFriction(PSparticle *part, uint8_t coefficient); // apply friction to specific particle - void applyFriction(uint8_t coefficient); // apply friction to all used particles + void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle + void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); @@ -164,7 +159,7 @@ class ParticleSystem void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die void setSaturation(uint8_t sat); //set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); + void setMotionBlur(uint8_t bluramount); //note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, uint8_t hardness = 255); @@ -172,13 +167,13 @@ class ParticleSystem PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) - uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this to set pointer to FX custom data + PSsizeControl *advPartSize; //pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 uint8_t numSources; //number of sources uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) private: //rendering functions @@ -192,7 +187,9 @@ class ParticleSystem void fireParticleupdate(); //utility functions - void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space + void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); //advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); @@ -200,6 +197,7 @@ class ParticleSystem CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed + PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint8_t wallHardness; @@ -215,10 +213,10 @@ class ParticleSystem }; //initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, bool advanced = false, uint16_t additionalbytes = 0); -uint32_t calculateNumberOfParticles(bool advanced); +bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, uint16_t additionalbytes); +bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); //color add function void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer diff --git a/wled00/data/index.js b/wled00/data/index.js index bbf6bd109c..d33fb63f70 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -272,6 +272,7 @@ function onLoad() selectSlot(0); updateTablinks(0); + handleLocationHash(); cpick.on("input:end", () => {setColor(1);}); cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); @@ -304,7 +305,6 @@ function updateTablinks(tabI) { var tablinks = gEBCN("tablinks"); for (var i of tablinks) i.classList.remove('active'); - if (pcMode) return; tablinks[tabI].classList.add('active'); } @@ -315,6 +315,21 @@ function openTab(tabI, force = false) _C.classList.toggle('smooth', false); _C.style.setProperty('--i', iSlide); updateTablinks(tabI); + switch (tabI) { + case 0: window.location.hash = "Colors"; break; + case 1: window.location.hash = "Effects"; break; + case 2: window.location.hash = "Segments"; break; + case 3: window.location.hash = "Presets"; break; + } +} + +function handleLocationHash() { + switch (window.location.hash) { + case "#Colors": openTab(0); break; + case "#Effects": openTab(1); break; + case "#Segments": openTab(2); break; + case "#Presets": openTab(3); break; + } } var timeout; @@ -3051,8 +3066,7 @@ function togglePcMode(fromB = false) pcMode = (wW >= 1024) && pcModeA; if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() - openTab(0, true); - updateTablinks(0); + if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); @@ -3214,6 +3228,7 @@ size(); _C.style.setProperty('--n', N); window.addEventListener('resize', size, true); +window.addEventListener('hashchange', handleLocationHash); _C.addEventListener('mousedown', lock, false); _C.addEventListener('touchstart', lock, false); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2461ebb285..a6ff9d096d 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -143,20 +143,8 @@ void handleImprovWifiScan(); void sendImprovIPRPCResult(ImprovRPCType type); //ir.cpp -void applyRepeatActions(); -byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF); -void decodeIR(uint32_t code); -void decodeIR24(uint32_t code); -void decodeIR24OLD(uint32_t code); -void decodeIR24CT(uint32_t code); -void decodeIR40(uint32_t code); -void decodeIR44(uint32_t code); -void decodeIR21(uint32_t code); -void decodeIR6(uint32_t code); -void decodeIR9(uint32_t code); -void decodeIRJson(uint32_t code); - void initIR(); +void deInitIR(); void handleIR(); //json.cpp diff --git a/wled00/ir.cpp b/wled00/ir.cpp index ba34aa5269..e475198f67 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -1,20 +1,14 @@ #include "wled.h" +#ifndef WLED_DISABLE_INFRARED #include "ir_codes.h" /* - * Infrared sensor support for generic 24/40/44 key RGB remotes + * Infrared sensor support for several generic RGB remotes and custom JSON remote */ -#if defined(WLED_DISABLE_INFRARED) -void handleIR(){} -#else - IRrecv* irrecv; -//change pin in NpbWrapper.h - decode_results results; - unsigned long irCheckedTime = 0; uint32_t lastValidCode = 0; byte lastRepeatableAction = ACTION_NONE; @@ -35,16 +29,16 @@ uint8_t lastIR6ColourIdx = 0; // print("%d values: %s" % (len(result), result)) // // It would be hard to maintain repeatable steps if calculating this on the fly. -const byte brightnessSteps[] = { +const uint8_t brightnessSteps[] = { 5, 7, 9, 12, 16, 20, 26, 34, 43, 56, 72, 93, 119, 154, 198, 255 }; const size_t numBrightnessSteps = sizeof(brightnessSteps) / sizeof(uint8_t); // increment `bri` to the next `brightnessSteps` value -void incBrightness() +static void incBrightness() { // dumb incremental search is efficient enough for so few items - for (uint8_t index = 0; index < numBrightnessSteps; ++index) + for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { @@ -56,7 +50,7 @@ void incBrightness() } // decrement `bri` to the next `brightnessSteps` value -void decBrightness() +static void decBrightness() { // dumb incremental search is efficient enough for so few items for (int index = numBrightnessSteps - 1; index >= 0; --index) @@ -70,12 +64,12 @@ void decBrightness() } } -void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) +static void presetFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); } -byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte higherBoundary) +static byte relativeChange(byte property, int8_t amount, byte lowerBoundary = 0, byte higherBoundary = 0xFF) { int16_t new_val = (int16_t) property + amount; if (lowerBoundary >= higherBoundary) return property; @@ -84,10 +78,10 @@ byte relativeChange(byte property, int8_t amount, byte lowerBoundary, byte highe return (byte)constrain(new_val, 0, 255); } -void changeEffect(uint8_t fx) +static void changeEffect(uint8_t fx) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; strip.setMode(i, fx); @@ -100,10 +94,10 @@ void changeEffect(uint8_t fx) stateChanged = true; } -void changePalette(uint8_t pal) +static void changePalette(uint8_t pal) { if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.setPalette(pal); @@ -116,13 +110,13 @@ void changePalette(uint8_t pal) stateChanged = true; } -void changeEffectSpeed(int8_t amount) +static void changeEffectSpeed(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectSpeed + amount; effectSpeed = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.speed = effectSpeed; @@ -134,10 +128,7 @@ void changeEffectSpeed(int8_t amount) } } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); + CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv_approximate(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 @@ -145,7 +136,7 @@ void changeEffectSpeed(int8_t amount) prim_hsv.h = (byte)new_val; hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -163,13 +154,13 @@ void changeEffectSpeed(int8_t amount) lastRepeatableValue = amount; } -void changeEffectIntensity(int8_t amount) +static void changeEffectIntensity(int8_t amount) { if (effectCurrent != 0) { int16_t new_val = (int16_t) effectIntensity + amount; effectIntensity = (byte)constrain(new_val,0,255); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.intensity = effectIntensity; @@ -181,16 +172,13 @@ void changeEffectIntensity(int8_t amount) } } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); - CRGB fastled_col; - fastled_col.red = R(sseg.colors[0]); - fastled_col.green = G(sseg.colors[0]); - fastled_col.blue = B(sseg.colors[0]); + CRGB fastled_col = CRGB(sseg.colors[0]); CHSV prim_hsv = rgb2hsv_approximate(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col); if (irApplyToAllSelected) { - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; seg.colors[0] = RGBW32(fastled_col.red, fastled_col.green, fastled_col.blue, W(sseg.colors[0])); @@ -208,11 +196,11 @@ void changeEffectIntensity(int8_t amount) lastRepeatableValue = amount; } -void changeColor(uint32_t c, int16_t cct=-1) +static void changeColor(uint32_t c, int16_t cct=-1) { if (irApplyToAllSelected) { // main segment may not be selected! - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; byte capabilities = seg.getLightCapabilities(); @@ -249,7 +237,7 @@ void changeColor(uint32_t c, int16_t cct=-1) stateChanged = true; } -void changeWhite(int8_t amount, int16_t cct=-1) +static void changeWhite(int8_t amount, int16_t cct=-1) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -259,72 +247,7 @@ void changeWhite(int8_t amount, int16_t cct=-1) changeColor(RGBW32(r, g, b, w), cct); } -void decodeIR(uint32_t code) -{ - if (code == 0xFFFFFFFF) { - //repeated code, continue brightness up/down - irTimesRepeated++; - applyRepeatActions(); - return; - } - lastValidCode = 0; irTimesRepeated = 0; - lastRepeatableAction = ACTION_NONE; - - if (irEnabled == 8) { // any remote configurable with ir.json file - decodeIRJson(code); - stateUpdated(CALL_MODE_BUTTON); - return; - } - if (code > 0xFFFFFF) return; //invalid code - - switch (irEnabled) { - case 1: - if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values - else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 - break; - case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys - case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys - case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys - case 5: decodeIR21(code); break; // white 21-key remote - case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, - // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" - // sets bright plain white - case 7: decodeIR9(code); break; - //case 8: return; // ir.json file, handled above switch statement - } - - if (nightlightActive && bri == 0) nightlightActive = false; - stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input -} - -void applyRepeatActions() -{ - if (irEnabled == 8) { - decodeIRJson(lastValidCode); - return; - } else switch (lastRepeatableAction) { - case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; - default: break; - } - if (lastValidCode == IR40_WPLUS) { - changeWhite(10); - stateUpdated(CALL_MODE_BUTTON); - } else if (lastValidCode == IR40_WMINUS) { - changeWhite(-10); - stateUpdated(CALL_MODE_BUTTON); - } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { - nightlightActive = true; - nightlightStartTime = millis(); - stateUpdated(CALL_MODE_BUTTON); - } -} - -void decodeIR24(uint32_t code) +static void decodeIR24(uint32_t code) { switch (code) { case IR24_BRIGHTER : incBrightness(); break; @@ -356,7 +279,7 @@ void decodeIR24(uint32_t code) lastValidCode = code; } -void decodeIR24OLD(uint32_t code) +static void decodeIR24OLD(uint32_t code) { switch (code) { case IR24_OLD_BRIGHTER : incBrightness(); break; @@ -388,7 +311,7 @@ void decodeIR24OLD(uint32_t code) lastValidCode = code; } -void decodeIR24CT(uint32_t code) +static void decodeIR24CT(uint32_t code) { switch (code) { case IR24_CT_BRIGHTER : incBrightness(); break; @@ -420,7 +343,7 @@ void decodeIR24CT(uint32_t code) lastValidCode = code; } -void decodeIR40(uint32_t code) +static void decodeIR40(uint32_t code) { Segment& seg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); byte r = R(seg.colors[0]); @@ -473,7 +396,7 @@ void decodeIR40(uint32_t code) lastValidCode = code; } -void decodeIR44(uint32_t code) +static void decodeIR44(uint32_t code) { switch (code) { case IR44_BPLUS : incBrightness(); break; @@ -525,7 +448,7 @@ void decodeIR44(uint32_t code) lastValidCode = code; } -void decodeIR21(uint32_t code) +static void decodeIR21(uint32_t code) { switch (code) { case IR21_BRIGHTER: incBrightness(); break; @@ -554,7 +477,7 @@ void decodeIR21(uint32_t code) lastValidCode = code; } -void decodeIR6(uint32_t code) +static void decodeIR6(uint32_t code) { switch (code) { case IR6_POWER: toggleOnOff(); break; @@ -587,7 +510,7 @@ void decodeIR6(uint32_t code) lastValidCode = code; } -void decodeIR9(uint32_t code) +static void decodeIR9(uint32_t code) { switch (code) { case IR9_POWER : toggleOnOff(); break; @@ -628,7 +551,7 @@ the json file. "label": "Preset 1, fallback to Saw - Party if not found"}, } */ -void decodeIRJson(uint32_t code) +static void decodeIRJson(uint32_t code) { char objKey[10]; char fileName[16]; @@ -701,41 +624,102 @@ void decodeIRJson(uint32_t code) releaseJSONBufferLock(); } +static void applyRepeatActions() +{ + if (irEnabled == 8) { + decodeIRJson(lastValidCode); + return; + } else switch (lastRepeatableAction) { + case ACTION_BRIGHT_UP : incBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_BRIGHT_DOWN : decBrightness(); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_UP : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_SPEED_DOWN : changeEffectSpeed(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_UP : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + case ACTION_INTENSITY_DOWN : changeEffectIntensity(lastRepeatableValue); stateUpdated(CALL_MODE_BUTTON); return; + default: break; + } + if (lastValidCode == IR40_WPLUS) { + changeWhite(10); + stateUpdated(CALL_MODE_BUTTON); + } else if (lastValidCode == IR40_WMINUS) { + changeWhite(-10); + stateUpdated(CALL_MODE_BUTTON); + } else if ((lastValidCode == IR24_ON || lastValidCode == IR40_ON) && irTimesRepeated > 7 ) { + nightlightActive = true; + nightlightStartTime = millis(); + stateUpdated(CALL_MODE_BUTTON); + } +} + +static void decodeIR(uint32_t code) +{ + if (code == 0xFFFFFFFF) { + //repeated code, continue brightness up/down + irTimesRepeated++; + applyRepeatActions(); + return; + } + lastValidCode = 0; irTimesRepeated = 0; + lastRepeatableAction = ACTION_NONE; + + if (irEnabled == 8) { // any remote configurable with ir.json file + decodeIRJson(code); + stateUpdated(CALL_MODE_BUTTON); + return; + } + if (code > 0xFFFFFF) return; //invalid code + + switch (irEnabled) { + case 1: + if (code > 0xF80000) decodeIR24OLD(code); // white 24-key remote (old) - it sends 0xFF0000 values + else decodeIR24(code); // 24-key remote - 0xF70000 to 0xF80000 + break; + case 2: decodeIR24CT(code); break; // white 24-key remote with CW, WW, CT+ and CT- keys + case 3: decodeIR40(code); break; // blue 40-key remote with 25%, 50%, 75% and 100% keys + case 4: decodeIR44(code); break; // white 44-key remote with color-up/down keys and DIY1 to 6 keys + case 5: decodeIR21(code); break; // white 21-key remote + case 6: decodeIR6(code); break; // black 6-key learning remote defaults: "CH" controls brightness, + // "VOL +" controls effect, "VOL -" controls colour/palette, "MUTE" + // sets bright plain white + case 7: decodeIR9(code); break; + //case 8: return; // ir.json file, handled above switch statement + } + + if (nightlightActive && bri == 0) nightlightActive = false; + stateUpdated(CALL_MODE_BUTTON); //for notifier, IR is considered a button input +} + void initIR() { - if (irEnabled > 0) - { + if (irEnabled > 0) { irrecv = new IRrecv(irPin); - irrecv->enableIRIn(); + if (irrecv) irrecv->enableIRIn(); + } else irrecv = nullptr; +} + +void deInitIR() +{ + if (irrecv) { + irrecv->disableIRIn(); + delete irrecv; } + irrecv = nullptr; } void handleIR() { - if (irEnabled > 0 && millis() - irCheckedTime > 120 && !strip.isUpdating()) - { - irCheckedTime = millis(); - if (irEnabled > 0) - { - if (irrecv == NULL) - { - initIR(); return; - } - - if (irrecv->decode(&results)) - { - if (results.value != 0) // only print results if anything is received ( != 0 ) - { - if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) // Serial TX pin (GPIO 1 on ESP32 and ESP8266) - Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); - } - decodeIR(results.value); - irrecv->resume(); + unsigned long currentTime = millis(); + unsigned timeDiff = currentTime - irCheckedTime; + if (timeDiff > 120 && irEnabled > 0 && irrecv) { + if (strip.isUpdating() && timeDiff < 240) return; // be nice, but not too nice + irCheckedTime = currentTime; + if (irrecv->decode(&results)) { + if (results.value != 0) { // only print results if anything is received ( != 0 ) + if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) // Serial TX pin (GPIO 1 on ESP32 and ESP8266) + Serial.printf_P(PSTR("IR recv: 0x%lX\n"), (unsigned long)results.value); } - } else if (irrecv != NULL) - { - irrecv->disableIRIn(); - delete irrecv; irrecv = NULL; + decodeIR(results.value); + irrecv->resume(); } } } diff --git a/wled00/json.cpp b/wled00/json.cpp index ae8224ad32..f306eb3235 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -487,6 +487,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } + doAdvancePlaylist = root[F("np")] | doAdvancePlaylist; //advances to next preset in playlist when true + JsonObject wifi = root[F("wifi")]; if (!wifi.isNull()) { bool apMode = getBoolVal(wifi[F("ap")], apActive); diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 2e2e4a6bd2..5599824ef2 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -103,20 +103,17 @@ void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties colorFromDecOrHexString(col, payloadStr); colorUpdated(CALL_MODE_DIRECT_CHANGE); } else if (strcmp_P(topic, PSTR("/api")) == 0) { - if (!requestJSONBufferLock(15)) { - delete[] payloadStr; - payloadStr = nullptr; - return; - } - if (payloadStr[0] == '{') { //JSON API - deserializeJson(*pDoc, payloadStr); - deserializeState(pDoc->as()); - } else { //HTTP API - String apireq = "win"; apireq += '&'; // reduce flash string usage - apireq += payloadStr; - handleSet(nullptr, apireq); + if (requestJSONBufferLock(15)) { + if (payloadStr[0] == '{') { //JSON API + deserializeJson(*pDoc, payloadStr); + deserializeState(pDoc->as()); + } else { //HTTP API + String apireq = "win"; apireq += '&'; // reduce flash string usage + apireq += payloadStr; + handleSet(nullptr, apireq); + } + releaseJSONBufferLock(); } - releaseJSONBufferLock(); } else if (strlen(topic) != 0) { // non standard topic, check with usermods usermods.onMqttMessage(topic, payloadStr); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 19b26c2242..d6d8ba52a5 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -25,11 +25,11 @@ void _overlayAnalogClock() { if (secondPixel < analogClock12pixel) { - strip.setRange(analogClock12pixel, overlayMax, 0xFF0000); - strip.setRange(overlayMin, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, overlayMax, color_fade(0xFF0000, bri)); + strip.setRange(overlayMin, secondPixel, color_fade(0xFF0000, bri)); } else { - strip.setRange(analogClock12pixel, secondPixel, 0xFF0000); + strip.setRange(analogClock12pixel, secondPixel, color_fade(0xFF0000, bri)); } } if (analogClock5MinuteMarks) @@ -38,12 +38,12 @@ void _overlayAnalogClock() { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; - strip.setPixelColor(pix, 0x00FFAA); + strip.setPixelColor(pix, color_fade(0x00FFAA, bri)); } } - if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, 0xFF0000); - strip.setPixelColor(minutePixel, 0x00FF00); - strip.setPixelColor(hourPixel, 0x0000FF); + if (!analogClockSecondsTrail) strip.setPixelColor(secondPixel, color_fade(0xFF0000, bri)); + strip.setPixelColor(minutePixel, color_fade(0x00FF00, bri)); + strip.setPixelColor(hourPixel, color_fade(0x0000FF, bri)); } diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 67c4f60494..36235ab9ea 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -127,7 +127,7 @@ void handlePlaylist() { static unsigned long presetCycledTime = 0; if (currentPlaylist < 0 || playlistEntries == nullptr) return; - if (millis() - presetCycledTime > (100*playlistEntryDur)) { +if (millis() - presetCycledTime > (100 * playlistEntryDur) || doAdvancePlaylist) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; @@ -149,6 +149,7 @@ void handlePlaylist() { strip.setTransition(fadeTransition ? playlistEntries[playlistIndex].tr * 100 : 0); playlistEntryDur = playlistEntries[playlistIndex].dur; applyPresetFromPlaylist(playlistEntries[playlistIndex].preset); + doAdvancePlaylist = false; } } diff --git a/wled00/set.cpp b/wled00/set.cpp index d3382be187..a8de357570 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -104,7 +104,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } #ifndef WLED_DISABLE_INFRARED if (irPin>=0 && pinManager.isPinAllocated(irPin, PinOwner::IR)) { - pinManager.deallocatePin(irPin, PinOwner::IR); + deInitIR(); + pinManager.deallocatePin(irPin, PinOwner::IR); } #endif for (uint8_t s=0; sarg(F("IT")).toInt(); + initIR(); #endif irApplyToAllSelected = !request->hasArg(F("MSO")); @@ -901,6 +903,9 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) applyPreset(presetCycCurr); } + pos = req.indexOf(F("NP")); //advances to next preset in a playlist + if (pos > 0) doAdvancePlaylist = true; + //set brightness updateVal(req.c_str(), "&A=", &bri); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index eb78608516..6251735c32 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -505,6 +505,13 @@ void WLED::setup() initServer(); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); +#ifndef WLED_DISABLE_INFRARED + // init IR + DEBUG_PRINTLN(F("initIR")); + initIR(); + DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); +#endif + // Seed FastLED random functions with an esp random value, which already works properly at this point. #if defined(ARDUINO_ARCH_ESP32) const uint32_t seed32 = esp_random(); diff --git a/wled00/wled.h b/wled00/wled.h index 139c451f82..dd90e1d74c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2404120 +#define VERSION 2405030 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG @@ -645,6 +645,7 @@ WLED_GLOBAL byte timerWeekday[] _INIT_N(({ 255, 255, 255, 255, 255, 255, 255, WLED_GLOBAL byte timerMonth[] _INIT_N(({28,28,28,28,28,28,28,28})); WLED_GLOBAL byte timerDay[] _INIT_N(({1,1,1,1,1,1,1,1})); WLED_GLOBAL byte timerDayEnd[] _INIT_N(({31,31,31,31,31,31,31,31})); +WLED_GLOBAL bool doAdvancePlaylist _INIT(false); //improv WLED_GLOBAL byte improvActive _INIT(0); //0: no improv packet received, 1: improv active, 2: provisioning From 2458a85ca10b90890a665790901ae068fb1b48ed Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 6 May 2024 19:48:29 +0200 Subject: [PATCH 14/21] remove esp_random() as it is not supported on ESP8266 --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index af9d828744..c430104eb6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8117,7 +8117,7 @@ uint16_t mode_particlefireworks(void) currentspeed = speed; counter = 0; angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) - angle = esp_random(); // random start angle + angle = random16(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles percircle = (uint16_t)0xFFFF / angleincrement + 1; @@ -8310,7 +8310,7 @@ uint16_t mode_particlefire(void) { if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = esp_random(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = random(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } From b12f20cc48e9e526583044754040382afd24c8a7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 May 2024 21:54:02 +0200 Subject: [PATCH 15/21] - added WLED_DISABLE_PARTICLESYSTEM option & cleanup & bugfixes - cleanup / reformatting - fixed volcano movement - small bugfix for ESP32 (random() does not work, using random16() ) --- wled00/FX.cpp | 349 +++-- wled00/FXparticleSystem.cpp | 2394 ++++++++++++++++++----------------- wled00/FXparticleSystem.h | 132 +- 3 files changed, 1438 insertions(+), 1437 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a370f02be0..22d9ede777 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7896,6 +7896,8 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; +#ifndef WLED_DISABLE_PARTICLESYSTEM + /* * Particle System Vortex * Particles sprayed from center with a rotating spray @@ -7913,18 +7915,18 @@ uint16_t mode_particlevortex(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed - //SEGMENT.aux0 = 0; // starting angle + //SEGMENT.aux0 = 0; // starting angle SEGMENT.aux1 = 0x01; // check flags - #ifdef ESP8266 + #ifdef ESP8266 PartSys->setMotionBlur(150); #else PartSys->setMotionBlur(100); #endif uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) - { + { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center PartSys->sources[i].maxLife = 900; @@ -7943,8 +7945,8 @@ uint16_t mode_particlevortex(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 - #ifdef ESP8266 - for (i = 1; i < 4; i++) //need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + #ifdef ESP8266 + for (i = 1; i < 4; i++) // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed { PartSys->particles[PartSys->numParticles - i].x = (PartSys->maxX + 1) >> 1; // center PartSys->particles[PartSys->numParticles - i].y = (PartSys->maxY + 1) >> 1; // center @@ -7952,7 +7954,7 @@ uint16_t mode_particlevortex(void) PartSys->particles[PartSys->numParticles - i].ttl = 255; //set alive } #endif - if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) //state change + if (SEGMENT.check1 != (SEGMENT.aux1 & 0x01) || SEGMENT.call == 0) // state change { if (SEGMENT.check1) SEGMENT.aux1 |= 0x01; //set the flag @@ -7976,7 +7978,7 @@ uint16_t mode_particlevortex(void) // set rotation direction and speed // can use direction flag to determine current direction bool direction = SEGMENT.check2; //no automatic direction change, set it to flag - int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step + int32_t currentspeed = (int32_t)SEGMENT.step; // make a signed integer out of step if (SEGMENT.custom2 > 0) // automatic direction change enabled { @@ -7993,8 +7995,8 @@ uint16_t mode_particlevortex(void) SEGMENT.aux1 |= 0x04; // set the update flag (for random interval update) if (direction) SEGMENT.aux1 &= ~0x02; // clear the direction flag - else - SEGMENT.aux1 |= 0x02; // set the direction flag + else + SEGMENT.aux1 |= 0x02; // set the direction flag } } @@ -8010,7 +8012,7 @@ uint16_t mode_particlevortex(void) speedincrement = 1; } - currentspeed += speedincrement; + currentspeed += speedincrement; SEGMENT.aux0 += currentspeed; SEGMENT.step = (uint32_t)currentspeed; //save it back @@ -8059,14 +8061,14 @@ uint16_t mode_particlefireworks(void) { if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed - PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setWallHardness(100); //ground bounce is fixed + PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(100); //ground bounce is fixed numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched - } + } } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8079,9 +8081,9 @@ uint16_t mode_particlefireworks(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); - PartSys->setWrapX(SEGMENT.check1); + PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); - PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // todo: make it a slider to adjust + PartSys->setGravity(map(SEGMENT.custom3,0,31,0,10)); // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8109,7 +8111,7 @@ uint16_t mode_particlefireworks(void) else // speed is zero, explode! { #ifdef ESP8266 - emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + emitparticles = random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion #else emitparticles = random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion #endif @@ -8120,7 +8122,7 @@ uint16_t mode_particlefireworks(void) speed = 2 + random16(3); currentspeed = speed; counter = 0; - angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) + angleincrement = 2730 + random16(5461); // minimum 15° (=2730), + random(30°) (=5461) angle = random16(); // random start angle speedvariation = angle & 0x01; // 0 or 1, no need for a new random number // calculate the number of particles to make complete circles @@ -8153,7 +8155,7 @@ uint16_t mode_particlefireworks(void) else { /* - //TODO: this does not look good. adjust or remove completely + //TODO: this does not look good. adjust or remove completely if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { if(i < (emitparticles>>2)) //set 1/4 of particles to larger size @@ -8171,7 +8173,7 @@ uint16_t mode_particlefireworks(void) } if(i == 0) //no particles emitted, this rocket is falling PartSys->sources[j].source.y = 1000; // set position up high so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) - circularexplosion = false; //reset for next rocket + circularexplosion = false; // reset for next rocket } // update the rockets, set the speed state @@ -8196,8 +8198,8 @@ uint16_t mode_particlefireworks(void) // reinitialize rocket PartSys->sources[j].source.y = PS_P_RADIUS<<1; // start from bottom PartSys->sources[j].source.x = random(PartSys->maxX >> 2, PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse - PartSys->sources[j].source.vx = random(-3,3); //i.e. not perfectly straight up + PartSys->sources[j].source.vy = random16(SEGMENT.custom1 >> 3) + 5; // rocket speed depends also on rocket fuse + PartSys->sources[j].source.vx = random(-3,3); // not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // sets explosion height (rockets explode at the top if set too high as paticle update set speed to zero if moving out of matrix) PartSys->sources[j].maxLife = 40; // exhaust particle life @@ -8208,7 +8210,7 @@ uint16_t mode_particlefireworks(void) } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } #undef NUMBEROFSOURCES @@ -8226,25 +8228,28 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint8_t numSprays; //note: so far only one tested but more is possible + PSsettings volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled + uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); //anable motion blur + PartSys->setMotionBlur(190); // anable motion blur numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = random16(); - PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); //distribute evenly + PartSys->sources[i].source.hue = random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly PartSys->sources[i].maxLife = 300; // lifetime in frames PartSys->sources[i].minLife = 250; - PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[i].source.perpetual = true; // source never dies } } @@ -8263,7 +8268,7 @@ uint16_t mode_particlevolcano(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByAge(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setWallHardness(SEGMENT.custom2); if (SEGMENT.check3) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8277,16 +8282,13 @@ uint16_t mode_particlevolcano(void) { PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') - PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source - // percycle = 1+(SEGMENT.intensity>>4); //how many particles are sprayed per cycle and how fast ist the color changing - PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 4 : -(SEGMENT.custom1 >> 4); // set moving speed but keep the direction + PartSys->sources[i].source.hue++; // = random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? SEGMENT.custom1 >> 2 : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed PartSys->sources[i].vx = 0; - PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) - // spray[j].source.hue = random16(); //set random color for each particle (using palette) -> does not look good + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) PartSys->sprayEmit(PartSys->sources[i]); - PartSys->particleMoveUpdate(PartSys->sources[i].source); //move the source (also applies gravity, which is corrected for above, that is a hack but easier than creating more particlesettings) - //Serial.println("emit"); + PartSys->particleMoveUpdate(PartSys->sources[i].source, &volcanosettings); //move the source } } @@ -8308,13 +8310,13 @@ uint16_t mode_particlefire(void) ParticleSystem *PartSys = NULL; uint32_t i; // index variable - uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed - SEGMENT.aux0 = random(); // aux0 is wind position (index) in the perlin noise + SEGMENT.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; DEBUG_PRINTF_P(PSTR("segment data ptr in fireFX %p\n"), SEGMENT.data); } @@ -8335,7 +8337,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.speed < 100) //slow, limit FPS { uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); - uint32_t period = strip.now - *lastcall; + uint32_t period = strip.now - *lastcall; if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) // limit to 90FPS - 20FPS { SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) @@ -8356,21 +8358,21 @@ uint16_t mode_particlefire(void) { if (PartSys->sources[i].source.ttl > 0) { - PartSys->sources[i].source.ttl--; + PartSys->sources[i].source.ttl--; } else // flame source is dead { - // initialize new flame: set properties of source + // initialize new flame: set properties of source if (random16(20) == 0 || SEGMENT.call == 0) // from time to time, change flame position - { - PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width + { + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread>>1) + random(spread); // distribute randomly on chosen width } PartSys->sources[i].source.y = -PS_P_RADIUS; // set the source below the frame PartSys->sources[i].source.ttl = 5 + random16((SEGMENT.custom1 * SEGMENT.custom1) >> 7) / (2 + (firespeed >> 4)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed -> new, this works! PartSys->sources[i].maxLife = random16(7) + 13; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height PartSys->sources[i].minLife = 4; - PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) - PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good + PartSys->sources[i].vx = (int8_t)random(-3, 3); // emitting speed (sideways) + PartSys->sources[i].vy = 5 + (firespeed >> 2); // emitting speed (upwards) -> this is good PartSys->sources[i].var = (random16(1 + (firespeed >> 5)) + 2); // speed variation around vx,vy (+/- var) } @@ -8379,7 +8381,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call & 0x01) // update noise position every second frames, also add wind { SEGMENT.aux0++; // position in the perlin noise matrix for wind generation - if (SEGMENT.call & 0x02) //every third frame + if (SEGMENT.call & 0x02) // every third frame SEGMENT.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles int8_t windspeed = ((int16_t)(inoise8(SEGMENT.aux0, SEGMENT.aux1) - 127) * SEGMENT.custom2) >> 7; @@ -8407,7 +8409,7 @@ uint16_t mode_particlefire(void) for(i=0; i < percycle; i++) { j = (j + 1) % numFlames; - PartSys->flameEmit(PartSys->sources[j]); + PartSys->flameEmit(PartSys->sources[j]); } PartSys->updateFire(SEGMENT.intensity); // update and render the fire @@ -8431,11 +8433,11 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) //init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); - PartSys->setGravity(); //enable with default gravity - PartSys->setUsedParticles((PartSys->numParticles*3)/2); //use 2/3 of available particles + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles((PartSys->numParticles*3)/2); // use 2/3 of available particles } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -8448,8 +8450,8 @@ uint16_t mode_particlepit(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); - PartSys->setBounceY(SEGMENT.check3); - PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); //limit to 100 min (if collisions are disabled, still want bouncy) + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) if (SEGMENT.custom2>0) { PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8458,7 +8460,7 @@ uint16_t mode_particlepit(void) PartSys->enableParticleCollisions(false); } - uint32_t i; + uint32_t i; if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) // every nth frame emit particles, stop emitting if set to zero { for (i = 0; i < PartSys->usedParticles; i++) // emit particles @@ -8471,19 +8473,19 @@ uint16_t mode_particlepit(void) PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].collide = true; //enable collision for particle + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; // enable collision for particle PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; - //set particle size + // set particle size if(SEGMENT.custom1 == 255) - { - PartSys->setParticleSize(0); //set global size to zero - PartSys->advPartProps[i].size = random(SEGMENT.custom1); //set each particle to random size + { + PartSys->setParticleSize(0); // set global size to zero + PartSys->advPartProps[i].size = random(SEGMENT.custom1); // set each particle to random size } else { - PartSys->setParticleSize(SEGMENT.custom1); //set global size - PartSys->advPartProps[i].size = 0; //use global size + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = 0; // use global size } break; // emit only one particle per round } @@ -8496,7 +8498,7 @@ uint16_t mode_particlepit(void) //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) - if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) //note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render @@ -8559,16 +8561,16 @@ uint16_t mode_particlewaterfall(void) { if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setGravity(); // enable with default gforce + PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setMotionBlur(190); //anable motion blur + PartSys->setMotionBlur(190); // anable motion blur numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); PartSys->sources[i].source.collide = true; // seeded particles will collide #ifdef ESP8266 - PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) PartSys->sources[i].minLife = 100; #else PartSys->sources[i].maxLife = 400; // lifetime in frames @@ -8602,7 +8604,7 @@ uint16_t mode_particlewaterfall(void) for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue++; //change hue of spray source + PartSys->sources[i].source.hue++; // change hue of spray source } if (SEGMENT.call % (9 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) // every nth frame, cycle color and emit particles, do not emit if intensity is zero @@ -8647,20 +8649,20 @@ uint16_t mode_particlebox(void) if (!initParticleSystem(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setBounceX(true); - PartSys->setBounceY(true); - //set max number of particles and save to aux1 for later + PartSys->setBounceY(true); + // set max number of particles and save to aux1 for later #ifdef ESP8266 SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); //max number of particles - #endif + SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles + #endif for (i = 0; i < SEGMENT.aux1; i++) { - PartSys->particles[i].ttl = 500; //set all particles alive (not all are rendered though) - PartSys->particles[i].perpetual = true; //never die - PartSys->particles[i].hue = i * 3; // color range + PartSys->particles[i].ttl = 500; // set all particles alive (not all are rendered though) + PartSys->particles[i].perpetual = true; // never die + PartSys->particles[i].hue = i * 3; // color range PartSys->particles[i].x = map(i, 0, SEGMENT.aux1, 1, PartSys->maxX); // distribute along x according to color - PartSys->particles[i].y = random16(PartSys->maxY >> 2); //bottom quarter + PartSys->particles[i].y = random16(PartSys->maxY >> 2); // bottom quarter PartSys->particles[i].collide = true; // all particles collide } SEGMENT.aux0 = rand(); // position in perlin noise @@ -8678,7 +8680,7 @@ uint16_t mode_particlebox(void) PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); //aux1 holds max number of particles to use + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, SEGMENT.aux1)); // aux1 holds max number of particles to use if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) // how often the force is applied depends on speed setting @@ -8686,12 +8688,12 @@ uint16_t mode_particlebox(void) int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - if(SEGMENT.check2) //direction + if(SEGMENT.check2) // direction SEGMENT.aux0 += increment; // update counter else SEGMENT.aux0 -= increment; - if(SEGMENT.check1) //random, use perlin noise + if(SEGMENT.check1) // random, use perlin noise { xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 127); ygravity = ((int16_t)inoise8(SEGMENT.aux0 + 10000) - 127); @@ -8699,12 +8701,12 @@ uint16_t mode_particlebox(void) xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; } - else //go in a circle + else // go in a circle { xgravity = ((int32_t)(SEGMENT.custom1) * cos16(SEGMENT.aux0 << 8)) / 0xFFFF; ygravity = ((int32_t)(SEGMENT.custom1) * sin16(SEGMENT.aux0 << 8)) / 0xFFFF; } - if (SEGMENT.check3) //sloshing, y force is alwys downwards + if (SEGMENT.check3) // sloshing, y force is alwys downwards { if(ygravity > 0) ygravity = -ygravity; @@ -8736,8 +8738,8 @@ uint16_t mode_particleperlin(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); //should never happen, but lets make sure there are no stray particles + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); for (i = 0; i < PartSys->numParticles; i++) { @@ -8756,7 +8758,7 @@ uint16_t mode_particleperlin(void) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(!SEGMENT.check1); PartSys->setBounceY(true); - PartSys->setWallHardness(SEGMENT.custom1); //wall hardness + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles>>1); PartSys->setUsedParticles(displayparticles); @@ -8805,21 +8807,19 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings;// = {0, 0, 0, 1, 0, 1, 0, 0}; // PS settings for meteors: bounceY and gravity enabled - meteorsettings.asByte = 0b00101000; - //uint8_t *settingsPtr = reinterpret_cast(&meteorsettings); // access settings as one byte (wmore efficient in code and speed) - //*settingsPtr = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + PSsettings meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(false); //explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) - PartSys->setGravity(); //enable default gravity - PartSys->setBounceY(true); //always use ground bounce + PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) - { + { PartSys->sources[i].source.y = 500; PartSys->sources[i].source.ttl = random16(20 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched @@ -8880,7 +8880,7 @@ uint16_t mode_particleimpact(void) { if (PartSys->sources[i].source.ttl) { - PartSys->sources[i].source.ttl--; //note: this saves an if statement, but moving down particles age twice + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice if (PartSys->sources[i].source.vy < 0) //move down { PartSys->applyGravity(&PartSys->sources[i].source); @@ -8888,10 +8888,9 @@ uint16_t mode_particleimpact(void) // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) // reached the bottom pixel on its way down - { + { PartSys->sources[i].source.vy = 0; // set speed zero so it will explode PartSys->sources[i].source.vx = 0; - //PartSys->sources[i].source.y = 5; // offset from ground so explosion happens not out of frame (!!!TODO: still needed? the class takes care of that) PartSys->sources[i].source.collide = true; #ifdef ESP8266 PartSys->sources[i].maxLife = 130; @@ -8905,7 +8904,7 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } - } + } } else if (PartSys->sources[i].source.vy > 0) // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it { @@ -8918,13 +8917,13 @@ uint16_t mode_particleimpact(void) PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].source.collide = false; // trail particles will not collide PartSys->sources[i].maxLife = 60; // spark particle life - PartSys->sources[i].minLife = 20; + PartSys->sources[i].minLife = 20; PartSys->sources[i].vy = -9; // emitting speed (down) - PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } #undef NUMBEROFSOURCES @@ -8941,22 +8940,22 @@ uint16_t mode_particleattractor(void) { if (SEGLEN == 1) return mode_static(); - ParticleSystem *PartSys = NULL; + ParticleSystem *PartSys = NULL; uint32_t i = 0; - PSsettings sourcesettings;// = {0, 0, 1, 1, 0, 0, 0, 0}; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) - sourcesettings.asByte = 0b00001100; - PSparticle *attractor; //particle pointer to the attractor - if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + PSsettings sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings - return mode_static(); // allocation failed; //allocation failed + return mode_static(); // allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); - PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].source.vx = -7; //will collied with wall and get random bounce direction - PartSys->sources[0].source.collide = true; // seeded particles will collide + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].source.collide = true; // seeded particles will collide PartSys->sources[0].source.perpetual = true; //source does not age #ifdef ESP8266 - PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) PartSys->sources[0].minLife = 30; #else PartSys->sources[0].maxLife = 350; // lifetime in frames @@ -8972,7 +8971,7 @@ uint16_t mode_particleattractor(void) if (PartSys == NULL) { DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! } // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8984,11 +8983,10 @@ uint16_t mode_particleattractor(void) PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness else PartSys->enableParticleCollisions(false); - uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; //use 3/4 of particles + uint16_t lastusedparticle = (PartSys->numParticles * 3) >> 2; // use 3/4 of particles uint32_t displayparticles = map(SEGMENT.intensity, 0, 255, 10, lastusedparticle); PartSys->setUsedParticles(displayparticles); // set pointers - //attractor = reinterpret_cast(&PartSys->particles[lastusedparticle + 1]); attractor = &PartSys->particles[lastusedparticle + 1]; if(SEGMENT.call == 0) { @@ -8997,7 +8995,7 @@ uint16_t mode_particleattractor(void) attractor->ttl = 100; attractor->perpetual = true; } - //set attractor properties + // set attractor properties if (SEGMENT.check2) { if((SEGMENT.call % 3) == 0) // move slowly @@ -9011,17 +9009,15 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call % 5 == 0) PartSys->sources[0].source.hue++; - SEGMENT.aux0 += 256; //emitting angle, one full turn in 255 frames (0xFFFF is 360°) + SEGMENT.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) if (SEGMENT.call % 2 == 0) // alternate direction of emit - PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); - //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, SEGMENT.custom1 >> 4); + PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0, 12); else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well - //PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, SEGMENT.custom1 >> 4); // apply force for(i = 0; i < displayparticles; i++) { - PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); //TODO: there was a bug here, counters was always the same pointer, check if this works. + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); @@ -9033,8 +9029,7 @@ static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass /* -Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles -uses inverse square law like in planetary motion +Particle Line Attractor, an idea that is not finished and not working Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9148,6 +9143,8 @@ uint16_t mode_particleattractor(void) } static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Emit Speed,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=190,c2=0,o1=0,o2=0,o3=0"; */ + + /* Particle Spray, just a particle spray with many parameters Uses palette for particle color @@ -9160,7 +9157,7 @@ uint16_t mode_particlespray(void) return mode_static(); ParticleSystem *PartSys = NULL; //uint8_t numSprays; - const uint8_t hardness = 200; //collision hardness is fixed + const uint8_t hardness = 200; // collision hardness is fixed if (SEGMENT.call == 0) // initialization { @@ -9190,7 +9187,7 @@ uint16_t mode_particlespray(void) PartSys->setBounceX(!SEGMENT.check2); PartSys->setWrapX(SEGMENT.check2); PartSys->setWallHardness(hardness); - PartSys->setGravity(8 * SEGMENT.check1); //enable gravity if checked (8 is default strength) + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays if (SEGMENT.check3) // collisions enabled @@ -9200,12 +9197,12 @@ uint16_t mode_particlespray(void) // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles - { + { PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + // spray[j].source.hue = random16(); //set random color for each particle (using palette) PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); } @@ -9231,8 +9228,8 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, 1)) // init - return mode_static(); // allocation failed; //allocation failed - PartSys->setKillOutOfBounds(true); + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles } else @@ -9245,7 +9242,7 @@ uint16_t mode_particleGEQ(void) } uint32_t i; - // set particle system properties + // set particle system properties PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceX(SEGMENT.check2); @@ -9264,9 +9261,6 @@ uint16_t mode_particleGEQ(void) uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 //map the bands into 16 positions on x axis, emit some particles according to frequency loudness - //Idea: emit 20 particles at full loudness, can use a shift for that, for example shift by 4 or 5 - //in order to also emit particles for not so loud bands, get a bunch of particles based on frame counter and current loudness? - //implement it simply first, then add complexity... need to check what looks good i = 0; uint32_t bin; //current bin uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) @@ -9296,20 +9290,20 @@ uint16_t mode_particleGEQ(void) { if (PartSys->particles[i].ttl == 0) // find a dead particle { - //set particle properties TODO: could also use the spray... - PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames - PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); //position randomly, deviating half a bin width - PartSys->particles[i].y = PS_P_RADIUS; //tart at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? PartSys->particles[i].vy = emitspeed; - PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin + PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; } i++; } } - PartSys->update(); // update and render + PartSys->update(); // update and render return FRAMETIME; } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; @@ -9334,8 +9328,8 @@ uint16_t mode_particleghostrider(void) PartSys->sources[0].maxLife = 260; // lifetime in frames PartSys->sources[0].minLife = 250; PartSys->sources[0].source.x = random16(PartSys->maxX); - PartSys->sources[0].source.y = random16(PartSys->maxY); - SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); //angle increment + PartSys->sources[0].source.y = random16(PartSys->maxY); + SEGMENT.step = random(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment } else PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS @@ -9346,7 +9340,7 @@ uint16_t mode_particleghostrider(void) return mode_static(); // something went wrong, no data! } - if(SEGMENT.intensity > 0) //spiraling + if(SEGMENT.intensity > 0) // spiraling { if(SEGMENT.aux1) { @@ -9366,7 +9360,7 @@ uint16_t mode_particleghostrider(void) PartSys->setMotionBlur(SEGMENT.custom1); PartSys->sources[0].var = SEGMENT.custom3 >> 1; - //color by age (PS color by age always starts with hue = 255 so cannot use that) + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) if(SEGMENT.check1) { for(int i = 0; i < PartSys->usedParticles; i++) @@ -9375,25 +9369,23 @@ uint16_t mode_particleghostrider(void) } } - //enable/disable walls + // enable/disable walls ghostsettings.bounceX = SEGMENT.check2; ghostsettings.bounceY = SEGMENT.check2; - SEGMENT.aux0 += (int32_t)SEGMENT.step; //step is angle increment - uint16_t emitangle = SEGMENT.aux0 + 32767; //+180° + SEGMENT.aux0 += (int32_t)SEGMENT.step; // step is angle increment + uint16_t emitangle = SEGMENT.aux0 + 32767; // +180° int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); - int8_t newvx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; - int8_t newvy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; PartSys->sources[0].source.vx = ((int32_t)cos16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; - PartSys->sources[0].source.ttl = 500; //source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->sources[0].source.vy = ((int32_t)sin16(SEGMENT.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) PartSys->particleMoveUpdate(PartSys->sources[0].source, &ghostsettings); - //set head (steal one of the particles) + // set head (steal one of the particles) PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; PartSys->particles[PartSys->usedParticles-1].ttl = 255; - PartSys->particles[PartSys->usedParticles-1].sat = 0; - //emit two particles + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; @@ -9405,7 +9397,7 @@ static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@S /* -PS Blobs: large particles bouncing around +PS Blobs: large particles bouncing around, changing size and form Uses palette for particle color by DedeHai (Damian Schneider) */ @@ -9418,10 +9410,9 @@ uint16_t mode_particleblobs(void) if (SEGMENT.call == 0) { if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) - return mode_static(); // allocation failed; //allocation failed - //PartSys->setGravity(); //enable with default gravity + return mode_static(); // allocation failed PartSys->setBounceX(true); - PartSys->setBounceY(true); + PartSys->setBounceY(true); PartSys->setWallHardness(255); PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); @@ -9438,37 +9429,37 @@ uint16_t mode_particleblobs(void) PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); PartSys->enableParticleCollisions(SEGMENT.check2); - + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles { - if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) //speed changed or dead - { + if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead + { PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } - if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) //size changed or dead - PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); //set each particle to slightly randomized size + if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size - //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set if (PartSys->particles[i].ttl == 0) // find dead particle, renitialize - { + { PartSys->particles[i].ttl = 300 + random16(((uint16_t)SEGMENT.custom2 << 3) + 100); - PartSys->particles[i].x = random(PartSys->maxX); - PartSys->particles[i].y = random16(PartSys->maxY); - PartSys->particles[i].hue = random16(); // set random color - PartSys->particles[i].collide = true; //enable collision for particle - PartSys->advPartProps[i].size = 0; //start out small + PartSys->particles[i].x = random(PartSys->maxX); + PartSys->particles[i].y = random16(PartSys->maxY); + PartSys->particles[i].hue = random16(); // set random color + PartSys->particles[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small PartSys->advPartSize[i].asymmetry = random16(220); PartSys->advPartSize[i].asymdir = random16(255); - //set advanced size control properties + // set advanced size control properties PartSys->advPartSize[i].grow = true; PartSys->advPartSize[i].growspeed = 1 + random16(9); - PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); - PartSys->advPartSize[i].wobblespeed = 1 + random(3); - } + PartSys->advPartSize[i].shrinkspeed = 1 + random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + random(3); + } //PartSys->advPartSize[i].asymmetry++; - PartSys->advPartSize[i].pulsate = SEGMENT.check3; - PartSys->advPartSize[i].wobble = SEGMENT.check1; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; } SEGMENT.aux0 = SEGMENT.speed; //write state back SEGMENT.aux1 = SEGMENT.custom1; @@ -9482,7 +9473,7 @@ static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs, /* - * Particle rotating GEQ + * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) * Particles sprayed from center with a rotating spray * Uses palette for particle color * by DedeHai (Damian Schneider) @@ -9532,8 +9523,8 @@ uint16_t mode_particlecenterGEQ(void) } for (i = 0; i < numSprays; i++) { - PartSys->sources[i].source.hue = i*16; //even color distribution - PartSys->sources[i].source.sat = 255; // set saturation + PartSys->sources[i].source.hue = i*16; // even color distribution + PartSys->sources[i].source.sat = 255; // set saturation PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center PartSys->sources[i].source.vx = 0; @@ -9612,6 +9603,9 @@ uint16_t mode_particlecenterGEQ(void) } static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; */ + +#endif //WLED_DISABLE_PARTICLESYSTEM + #endif // WLED_DISABLE_2D @@ -9853,6 +9847,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio +#ifndef WLED_DISABLE_PARTICLESYSTEM addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -9867,8 +9862,8 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); +#endif // WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 3a8b4cf7ae..4787a4d7f2 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -6,7 +6,7 @@ LICENSE The MIT License (MIT) - Copyright (c) 2024 Damian Schneider + Copyright (c) 2024 Damian Schneider Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -22,13 +22,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ /* - Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ - it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit - this should be used to optimize speed but not if memory is affected much + Note on ESP32: using 32bit integer is faster than 16bit or 8bit, each operation takes on less instruction, can be testen on https://godbolt.org/ + it does not matter if using int, unsigned int, uint32_t or int32_t, the compiler will make int into 32bit + this should be used to optimize speed but not if memory is affected much */ /* @@ -37,7 +36,9 @@ -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not -add an x/y struct, do particle rendering using that, much easier to read */ -// sources need to be updatable by the FX, so functions are needed to apply it to a single particle that are public + +#ifndef WLED_DISABLE_PARTICLESYSTEM + #include "FXparticleSystem.h" #include "wled.h" #include "FastLED.h" @@ -45,412 +46,412 @@ ParticleSystem::ParticleSystem(uint16_t width, uint16_t height, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced, bool sizecontrol) { - //Serial.println("PS Constructor"); - numSources = numberofsources; - numParticles = numberofparticles; // set number of particles in the array - usedParticles = numberofparticles; // use all particles by default - advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) - advPartSize = NULL; - updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) - setMatrixSize(width, height); - setWallHardness(255); // set default wall hardness to max - setWallRoughness(0); // smooth walls by default - setGravity(0); //gravity disabled by default - setParticleSize(0); // minimum size by default - motionBlur = 0; //no fading by default - emitIndex = 0; - - //initialize some default non-zero values most FX use - for (int i = 0; i < numSources; i++) - { - sources[i].source.sat = 255; //set saturation to max by default - sources[i].source.ttl = 1; //set source alive - } - for (int i = 0; i < numParticles; i++) - { - particles[i].sat = 255; // full saturation - } - //Serial.println("PS Constructor done"); + //Serial.println("PS Constructor"); + numSources = numberofsources; + numParticles = numberofparticles; // set number of particles in the array + usedParticles = numberofparticles; // use all particles by default + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = NULL; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setMatrixSize(width, height); + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + emitIndex = 0; + + //initialize some default non-zero values most FX use + for (int i = 0; i < numSources; i++) + { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + } + for (int i = 0; i < numParticles; i++) + { + particles[i].sat = 255; // full saturation + } + //Serial.println("PS Constructor done"); } // update function applies gravity, moves the particles, handles collisions and renders the particles void ParticleSystem::update(void) { - PSadvancedParticle *advprop = NULL; - //apply gravity globally if enabled - if (particlesettings.useGravity) - applyGravity(); - - //update size settings before handling collisions - if(advPartSize) - { - for (int i = 0; i < usedParticles; i++) - { - updateSize(&advPartProps[i], &advPartSize[i]); - } - } - - // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) - if (particlesettings.useCollisions) - handleCollisions(); - - //move all particles - for (int i = 0; i < usedParticles; i++) - { - if(advPartProps) - { - advprop = &advPartProps[i]; - } - particleMoveUpdate(particles[i], &particlesettings, advprop); - } - - /*TODO remove this - Serial.print("alive particles: "); - uint32_t aliveparticles = 0; - for (int i = 0; i < numParticles; i++) - { - if(particles[i].ttl) - aliveparticles++; - } - Serial.println(aliveparticles); - */ - ParticleSys_render(); + PSadvancedParticle *advprop = NULL; + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize) + { + for (int i = 0; i < usedParticles; i++) + { + updateSize(&advPartProps[i], &advPartSize[i]); + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (int i = 0; i < usedParticles; i++) + { + if (advPartProps) + { + advprop = &advPartProps[i]; + } + particleMoveUpdate(particles[i], &particlesettings, advprop); + } + + /*TODO remove this + Serial.print("alive particles: "); + uint32_t aliveparticles = 0; + for (int i = 0; i < numParticles; i++) + { + if (particles[i].ttl) + aliveparticles++; + } + Serial.println(aliveparticles); + */ + ParticleSys_render(); } // update function for fire animation void ParticleSystem::updateFire(uint32_t intensity, bool renderonly) { - if(!renderonly) - fireParticleupdate(); - ParticleSys_render(true, intensity); + if (!renderonly) + fireParticleupdate(); + ParticleSys_render(true, intensity); } void ParticleSystem::setUsedParticles(uint16_t num) { - usedParticles = min(num, numParticles); //limit to max particles + usedParticles = min(num, numParticles); //limit to max particles } void ParticleSystem::setWallHardness(uint8_t hardness) { - wallHardness = hardness; + wallHardness = hardness; } void ParticleSystem::setWallRoughness(uint8_t roughness) { - wallRoughness = roughness; + wallRoughness = roughness; } void ParticleSystem::setCollisionHardness(uint8_t hardness) -{ - collisionHardness = hardness; +{ + collisionHardness = hardness; } void ParticleSystem::setMatrixSize(uint16_t x, uint16_t y) { - maxXpixel = x - 1; // last physical pixel that can be drawn to - maxYpixel = y - 1; - maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements - maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions } void ParticleSystem::setWrapX(bool enable) { - particlesettings.wrapX = enable; + particlesettings.wrapX = enable; } void ParticleSystem::setWrapY(bool enable) { - particlesettings.wrapY = enable; + particlesettings.wrapY = enable; } void ParticleSystem::setBounceX(bool enable) { - particlesettings.bounceX = enable; + particlesettings.bounceX = enable; } void ParticleSystem::setBounceY(bool enable) { - particlesettings.bounceY = enable; + particlesettings.bounceY = enable; } void ParticleSystem::setKillOutOfBounds(bool enable) { - particlesettings.killoutofbounds = enable; + particlesettings.killoutofbounds = enable; } void ParticleSystem::setColorByAge(bool enable) { - particlesettings.colorByAge = enable; + particlesettings.colorByAge = enable; } void ParticleSystem::setMotionBlur(uint8_t bluramount) { - if(particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) - motionBlur = bluramount; + if (particlesize == 0) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; } // render size using smearing (see blur function) void ParticleSystem::setParticleSize(uint8_t size) { - particlesize = size; - particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props - motionBlur = 0; // disable motion blur if particle size is set + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS + particlesize; // note: this sets size if not using advanced props + motionBlur = 0; // disable motion blur if particle size is set } // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is disable // if enabled, gravity is applied to all particles in ParticleSystemUpdate() // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) void ParticleSystem::setGravity(int8_t force) -{ - if (force) - { - gforce = force; - particlesettings.useGravity = true; - } - else - particlesettings.useGravity = false; +{ + if (force) + { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; } void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable { - particlesettings.useCollisions = enable; - collisionHardness = hardness + 1; + particlesettings.useCollisions = enable; + collisionHardness = hardness + 1; } // emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) int32_t ParticleSystem::sprayEmit(PSsource &emitter) { - for (int32_t i = 0; i < usedParticles; i++) - { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle - { - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); - particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); - if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - return i; - } - } - return -1; + for (int32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + return i; + } + } + return -1; } // Spray emitter for particles used for flames (particle TTL depends on source TTL) void ParticleSystem::flameEmit(PSsource &emitter) { - for (uint32_t i = 0; i < usedParticles; i++) - { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle - { - particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster - particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; - // fire uses ttl and not hue for heat, so no need to set the hue - break; // done - } - } - /* - // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment - uint32_t partidx = sprayEmit(emitter); //emit one particle - // adjust properties - particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base - particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL - */ + for (uint32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].x = emitter.source.x + random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].vx = emitter.vx + random16(emitter.var) - (emitter.var >> 1); // random16 is good enough for fire and much faster + particles[emitIndex].vy = emitter.vy + random16(emitter.var) - (emitter.var >> 1); + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife) + emitter.source.ttl; + // fire uses ttl and not hue for heat, so no need to set the hue + break; // done + } + } + /* + // note: this attemt to save on code size turns out to be much slower as fire uses a lot of particle emits, this must be efficient. also emitter.var would need adjustment + uint32_t partidx = sprayEmit(emitter); //emit one particle + // adjust properties + particles[partidx].x += random16(PS_P_RADIUS<<1) - PS_P_RADIUS; // jitter the flame by one pixel to make the flames wider at the base + particles[partidx].ttl += emitter.source.ttl; // flame intensity dies down with emitter TTL + */ } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) { - emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding - emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - sprayEmit(emitter); + emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + sprayEmit(emitter); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) { - if(options == NULL) - options = &particlesettings; //use PS system settings by default - if (part.ttl > 0) - { - if(!part.perpetual) - part.ttl--; // age - if (particlesettings.colorByAge) - part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl - - bool usesize = false; // particle uses individual size rendering - int32_t newX = part.x + (int16_t)part.vx; - int32_t newY = part.y + (int16_t)part.vy; - part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) - - if(advancedproperties) //using individual particle size? - { - if(advancedproperties->size > 0) - usesize = true; // note: variable eases out of frame checking below - particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); - } - // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view - if (options->bounceX) - { - if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall - bounce(part.vx, part.vy, newX, maxX); - } - - if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) - { - if (options->wrapX) - { - newX = (uint16_t)newX % (maxX + 1); - } - else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if(usesize) // using individual particle size - { - if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough - isleaving = false; - } - - if(isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - part.ttl = 0; - } - } - } - - if (options->bounceY) - { - if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling - { - if (newY < particleHardRadius) // bounce at bottom - bounce(part.vy, part.vx, newY, maxY); - else - { - if(!options->useGravity) - bounce(part.vy, part.vx, newY, maxY); - } - } - } - - if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) - { - if (options->wrapY) - { - newY = (uint16_t)newY % (maxY + 1); - } - else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left - { - bool isleaving = true; - if(usesize) // using individual particle size - { - if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach - isleaving = false; - } - if(isleaving) - { - part.outofbounds = 1; - if (options->killoutofbounds) - { - if (newY < 0) // if gravity is enabled, only kill particles below ground - part.ttl = 0; - else if (!options->useGravity) - part.ttl = 0; - } - } - } - } - part.x = (int16_t)newX; // set new position - part.y = (int16_t)newY; // set new position - } + if (options == NULL) + options = &particlesettings; //use PS system settings by default + if (part.ttl > 0) + { + if (!part.perpetual) + part.ttl--; // age + if (particlesettings.colorByAge) + part.hue = part.ttl > 255 ? 255 : part.ttl; //set color to ttl + + bool usesize = false; // particle uses individual size rendering + int32_t newX = part.x + (int16_t)part.vx; + int32_t newY = part.y + (int16_t)part.vy; + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (advancedproperties) //using individual particle size? + { + if (advancedproperties->size > 0) + usesize = true; // note: variable eases out of frame checking below + particleHardRadius = max(PS_P_MINHARDRADIUS, (int)particlesize + (advancedproperties->size)); + } + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounceX) + { + if ((newX < particleHardRadius) || (newX > maxX - particleHardRadius)) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) + { + if (options->wrapX) + { + newX = (uint16_t)newX % (maxX + 1); + } + else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + part.ttl = 0; + } + } + } + + if (options->bounceY) + { + if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling + { + if (newY < particleHardRadius) // bounce at bottom + bounce(part.vy, part.vx, newY, maxY); + else + { + if (!options->useGravity) + bounce(part.vy, part.vx, newY, maxY); + } + } + } + + if (((newY < 0) || (newY > maxY))) // check if particle reached an edge (makes sure particles are within frame for rendering) + { + if (options->wrapY) + { + newY = (uint16_t)newY % (maxY + 1); + } + else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newY > -particleHardRadius) && (newY < maxY + particleHardRadius))) // still withing rendering reach + isleaving = false; + } + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + } + } + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } } // update advanced particle size control void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { - if(advsize == NULL) // just a safety check - return; - // grow/shrink particle - int32_t newsize = advprops->size; - uint32_t counter = advsize->sizecounter; - uint32_t increment; - // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds - if(advsize->grow) increment = advsize->growspeed; - else if(advsize->shrink) increment = advsize->shrinkspeed; - if(increment < 9) // 8 means +1 every frame - { - counter += increment; - if(counter > 7) - { - counter -= 8; - increment = 1; - } - else - increment = 0; - advsize->sizecounter = counter; - } - else{ - increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 - } - if(advsize->grow) - { - if(newsize < advsize->maxsize) - { - newsize += increment; - if(newsize >= advsize->maxsize) - { - advsize->grow = false; // stop growing, shrink from now on if enabled - newsize = advsize->maxsize; // limit - if(advsize->pulsate) advsize->shrink = true; - } - } - } - else if(advsize->shrink) - { - if(newsize > advsize->minsize) - { - newsize -= increment; - if(newsize <= advsize->minsize) - { - //if(advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction - advsize->shrink = false; // disable shrinking - newsize = advsize->minsize; // limit - if(advsize->pulsate) advsize->grow = true; - } - } - } - advprops->size = newsize; - // handle wobbling - if(advsize->wobble) - { - advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... - } + if (advsize == NULL) // just a safety check + return; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) // 8 means +1 every frame + { + counter += increment; + if (counter > 7) + { + counter -= 8; + increment = 1; + } + else + increment = 0; + advsize->sizecounter = counter; + } + else{ + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + if (advsize->grow) + { + if (newsize < advsize->maxsize) + { + newsize += increment; + if (newsize >= advsize->maxsize) + { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } + else if (advsize->shrink) + { + if (newsize > advsize->minsize) + { + newsize -= increment; + if (newsize <= advsize->minsize) + { + //if (advsize->minsize == 0) part.ttl = 0; //TODO: need to pass particle or return kill instruction + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) + { + advsize->asymdir += advsize->wobblespeed; // todo: need better wobblespeed control? counter is already in the struct... + } } // calculate x and y size for asymmetrical particles (advanced size control) void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { - if(advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) - return; + if (advsize == NULL) // if advanced size is valid, also advced properties pointer is valid (handled by pointer assignment function) + return; int32_t deviation = ((uint32_t)advprops->size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) - if (advsize->asymdir < 64) { - deviation = ((int32_t)advsize->asymdir * deviation) / 64; + if (advsize->asymdir < 64) { + deviation = ((int32_t)advsize->asymdir * deviation) / 64; } else if (advsize->asymdir < 192) { deviation = ((128 - (int32_t)advsize->asymdir) * deviation) / 64; } else { @@ -464,23 +465,23 @@ void ParticleSystem::getParticleXYsize(PSadvancedParticle *advprops, PSsizeContr // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition) { - incomingspeed = -incomingspeed; - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface - if (position < particleHardRadius) - position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better - else - position = maxposition - particleHardRadius; - if(wallRoughness) - { - int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); - // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness - parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); - incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); - donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same - incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; - } + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) + { + int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = abs(incomingspeed); + donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); + donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } } // apply a force in x,y direction to individual particle @@ -488,44 +489,44 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) void ParticleSystem::applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter) { - // for small forces, need to use a delay counter - uint8_t xcounter = (*counter) & 0x0F; // lower four bits - uint8_t ycounter = (*counter) >> 4; // upper four bits + // for small forces, need to use a delay counter + uint8_t xcounter = (*counter) & 0x0F; // lower four bits + uint8_t ycounter = (*counter) >> 4; // upper four bits - // velocity increase - int32_t dvx = calcForce_dv(xforce, &xcounter); - int32_t dvy = calcForce_dv(yforce, &ycounter); + // velocity increase + int32_t dvx = calcForce_dv(xforce, &xcounter); + int32_t dvy = calcForce_dv(yforce, &ycounter); - // save counter values back - *counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits - *counter |= (ycounter << 4) & 0xF0; // write upper four bits + // save counter values back + *counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + *counter |= (ycounter << 4) & 0xF0; // write upper four bits - // apply the force to particle: - part->vx = limitSpeed((int32_t)part->vx + dvx); - part->vy = limitSpeed((int32_t)part->vy + dvy); + // apply the force to particle + part->vx = limitSpeed((int32_t)part->vx + dvx); + part->vy = limitSpeed((int32_t)part->vy + dvy); } // apply a force in x,y direction to individual particle using advanced particle properties void ParticleSystem::applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce) { - if (advPartProps == NULL) - return; // no advanced properties available - applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); + if (advPartProps == NULL) + return; // no advanced properties available + applyForce(&particles[particleindex], xforce, yforce, &advPartProps[particleindex].forcecounter); } // apply a force in x,y direction to all particles // force is in 3.4 fixed point notation (see above) void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) { - // for small forces, need to use a delay counter - uint8_t tempcounter; - // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough - for (uint i = 0; i < usedParticles; i++) - { - tempcounter = forcecounter; - applyForce(&particles[i], xforce, yforce, &tempcounter); - } - forcecounter = tempcounter; //save value back + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; i++) + { + tempcounter = forcecounter; + applyForce(&particles[i], xforce, yforce, &tempcounter); + } + forcecounter = tempcounter; //save value back } // apply a force in angular direction to single particle @@ -534,26 +535,26 @@ void ParticleSystem::applyForce(int8_t xforce, int8_t yforce) // force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) void ParticleSystem::applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower - applyForce(part, xforce, yforce, counter); + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + // note: sin16 is 10% faster than sin8() on ESP32 but on ESP8266 it is 9% slower + applyForce(part, xforce, yforce, counter); } void ParticleSystem::applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle) { - if (advPartProps == NULL) - return; // no advanced properties available - applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); + if (advPartProps == NULL) + return; // no advanced properties available + applyAngleForce(&particles[particleindex], force, angle, &advPartProps[particleindex].forcecounter); } // apply a force in angular direction to all particles // angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) { - int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 - int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - applyForce(xforce, yforce); + int8_t xforce = ((int32_t)force * cos16(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(xforce, yforce); } @@ -562,139 +563,139 @@ void ParticleSystem::applyAngleForce(int8_t force, uint16_t angle) // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem::applyGravity() { - int32_t dv = calcForce_dv(gforce, &gforcecounter); - for (uint32_t i = 0; i < usedParticles; i++) - { - // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways - particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); - } + int32_t dv = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) + { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); + } } // apply gravity to single particle using system settings (use this for sources) // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { - int32_t dv; // velocity increase - if (gforce > 15) - dv = (gforce >> 4); // apply the 4 MSBs - else - dv = 1; - - if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = limitSpeed((int32_t)part->vy - dv); - } + int32_t dv; // velocity increase + if (gforce > 15) + dv = (gforce >> 4); // apply the 4 MSBs + else + dv = 1; + + if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity + { + part->vy = limitSpeed((int32_t)part->vy - dv); + } } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) // note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that void ParticleSystem::applyFriction(PSparticle *part, int32_t coefficient) { - int32_t friction = 255 - coefficient; - // note: not checking if particle is dead can be done by caller (or can be omitted) - // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. - part->vx = ((int16_t)part->vx * friction) / 255; - part->vy = ((int16_t)part->vy * friction) / 255; + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead can be done by caller (or can be omitted) + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate or things start to go to the left side. + part->vx = ((int16_t)part->vx * friction) / 255; + part->vy = ((int16_t)part->vy * friction) / 255; } // apply friction to all particles void ParticleSystem::applyFriction(int32_t coefficient) { - for (uint32_t i = 0; i < usedParticles; i++) - { - if(particles[i].ttl) - applyFriction(&particles[i], coefficient); - } + for (uint32_t i = 0; i < usedParticles; i++) + { + if (particles[i].ttl) + applyFriction(&particles[i], coefficient); + } } // attracts a particle to an attractor particle using the inverse square-law void ParticleSystem::pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow) { - if (advPartProps == NULL) - return; // no advanced properties available - - // Calculate the distance between the particle and the attractor - int32_t dx = attractor->x - particles[particleindex].x; - int32_t dy = attractor->y - particles[particleindex].y; - - // Calculate the force based on inverse square law - int32_t distanceSquared = dx * dx + dy * dy; - if (distanceSquared < 8192) - { - if (swallow) // particle is close, age it fast so it fades out, do not attract further - { - if (particles[particleindex].ttl > 7) - particles[particleindex].ttl -= 8; - else - { - particles[particleindex].ttl = 0; - return; - } - } - distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces - } - - int32_t force = ((int32_t)strength << 16) / distanceSquared; - int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting - int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - - applyForce(particleindex, xforce, yforce); + if (advPartProps == NULL) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor->x - particles[particleindex].x; + int32_t dy = attractor->y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) + { + if (swallow) // particle is close, age it fast so it fades out, do not attract further + { + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else + { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + + applyForce(particleindex, xforce, yforce); } /* //attract to a line (TODO: this is not yet working) void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength) { - // Calculate the distance between the particle and the attractor - if(advPartProps == NULL) - return; // no advanced properties available - - // calculate a second point on the line - int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); - int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); - // calculate squared distance from particle to the line: - int32_t dx = (x1 - attractorcenter->x) >> 4; - int32_t dy = (y1 - attractorcenter->y) >> 4; - int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; - int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); - - - // Calculate the force based on inverse square law - if (distanceSquared < 2) - { - distanceSquared = 1; - // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces - } - - int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; - //apply force in a 90° angle to the line - int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting - int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - - Serial.print(" partx: "); - Serial.print(particles[particleindex].x); - Serial.print(" party "); - Serial.print(particles[particleindex].y); - Serial.print(" x1 "); - Serial.print(x1); - Serial.print(" y1 "); - Serial.print(y1); - Serial.print(" dx "); - Serial.print(dx); - Serial.print(" dy "); - Serial.print(dy); - Serial.print(" d: "); - Serial.print(d); - Serial.print(" dsq: "); - Serial.print(distanceSquared); - Serial.print(" force: "); - Serial.print(force); - Serial.print(" fx: "); - Serial.print(xforce); - Serial.print(" fy: "); - Serial.println(yforce); - - applyForce(particleindex, xforce, yforce); + // Calculate the distance between the particle and the attractor + if (advPartProps == NULL) + return; // no advanced properties available + + // calculate a second point on the line + int32_t x1 = attractorcenter->x + (cos16(attractorangle) >> 5); + int32_t y1 = attractorcenter->y + (sin16(attractorangle) >> 5); + // calculate squared distance from particle to the line: + int32_t dx = (x1 - attractorcenter->x) >> 4; + int32_t dy = (y1 - attractorcenter->y) >> 4; + int32_t d = ((dx * (particles[particleindex].y - attractorcenter->y)) - (dy * (particles[particleindex].x - attractorcenter->x))) >> 8; + int32_t distanceSquared = (d * d) / (dx * dx + dy * dy); + + + // Calculate the force based on inverse square law + if (distanceSquared < 2) + { + distanceSquared = 1; + // distanceSquared = 4 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = (((int32_t)strength << 16) / distanceSquared)>>10; + //apply force in a 90° angle to the line + int8_t xforce = (d > 0 ? 1 : -1) * (force * dy) / 100; // scale to a lower value, found by experimenting + int8_t yforce = (d > 0 ? -1 : 1) * (force * dx) / 100; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + + Serial.print(" partx: "); + Serial.print(particles[particleindex].x); + Serial.print(" party "); + Serial.print(particles[particleindex].y); + Serial.print(" x1 "); + Serial.print(x1); + Serial.print(" y1 "); + Serial.print(y1); + Serial.print(" dx "); + Serial.print(dx); + Serial.print(" dy "); + Serial.print(dy); + Serial.print(" d: "); + Serial.print(d); + Serial.print(" dsq: "); + Serial.print(distanceSquared); + Serial.print(" force: "); + Serial.print(force); + Serial.print(" fx: "); + Serial.print(xforce); + Serial.print(" fy: "); + Serial.println(yforce); + + applyForce(particleindex, xforce, yforce); }*/ // render particles to the LED buffer (uses palette to render the 8bit particle color value) @@ -703,355 +704,355 @@ void ParticleSystem::lineAttractor(uint16_t particleindex, PSparticle *attractor // fireintensity and firemode are optional arguments (fireintensity is only used in firemode) void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) { - - CRGB baseRGB; - bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) - CRGB **framebuffer = NULL; //local frame buffer - CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles - uint32_t i; - uint32_t brightness; // particle brightness, fades if dying - - if (useLocalBuffer) - { - /* - //memory fragmentation check: - Serial.print("heap: "); - Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); - Serial.print(" block: "); - Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); - */ - - // allocate empty memory for the local renderbuffer - framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); - if (framebuffer == NULL) - { - Serial.println("Frame buffer alloc failed"); - useLocalBuffer = false; //render to segment pixels directly if not enough memory - } - else{ - if(advPartProps) - { - renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0 note: null checking is done when accessing it - } - if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation - { - uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) - { - yflipped = maxYpixel - y; - for (uint32_t x = 0; x <= maxXpixel; x++) - { - framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer - fast_color_scale(framebuffer[x][y], motionBlur); - } - } - } - - } - - } - - if(!useLocalBuffer) //disabled or allocation above failed - { - Serial.println("NOT using local buffer!"); - if (motionBlur > 0) - SEGMENT.fadeToBlackBy(255 - motionBlur); - else - SEGMENT.fill(BLACK); //clear the buffer before rendering to it - } - // go over particles and render them to the buffer - for (i = 0; i < usedParticles; i++) - { - if (particles[i].outofbounds || particles[i].ttl == 0) - continue; - - // generate RGB values for particle - if(firemode) - { - //TODO: decide on a final version... - //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good - //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental - //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky - brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! - brightness = brightness > 255 ? 255 : brightness; // faster then using min() - baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); - } - else{ - brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() - baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); - if (particles[i].sat < 255) - { - CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV - baseHSV.s = particles[i].sat; //set the saturation - baseRGB = (CRGB)baseHSV; // convert back to RGB - } - } - - renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); - } - - if(particlesize > 0) - { - uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max - uint32_t bluramount = particlesize; - uint32_t bitshift = 0; - - for(int i = 0; i < passes; i++) - { - if(i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) - bitshift = 1; - - if (useLocalBuffer) - blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); - else - SEGMENT.blur(bluramount << bitshift, true); - bluramount -= 64; - } - } - - if (useLocalBuffer) // transfer local buffer back to segment - { - uint32_t yflipped; - for (int y = 0; y <= maxYpixel; y++) - { - yflipped = maxYpixel - y; - for (int x = 0; x <= maxXpixel; x++) - { - SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); - } - } - free(framebuffer); - } - if(renderbuffer) - free(renderbuffer); + + CRGB baseRGB; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) + CRGB **framebuffer = NULL; //local frame buffer + CRGB **renderbuffer = NULL; //local particle render buffer for advanced particles + uint32_t i; + uint32_t brightness; // particle brightness, fades if dying + + if (useLocalBuffer) + { + /* + //memory fragmentation check: + Serial.print("heap: "); + Serial.print(heap_caps_get_free_size(MALLOC_CAP_8BIT)); + Serial.print(" block: "); + Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)); + */ + + // allocate empty memory for the local renderbuffer + framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); + if (framebuffer == NULL) + { + Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + if (advPartProps) + { + renderbuffer = allocate2Dbuffer(10, 10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + } + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + uint32_t yflipped; + for (uint32_t y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer + fast_color_scale(framebuffer[x][y], motionBlur); + } + } + } + + } + + } + + if (!useLocalBuffer) //disabled or allocation above failed + { + Serial.println("NOT using local buffer!"); + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it + } + // go over particles and render them to the buffer + for (i = 0; i < usedParticles; i++) + { + if (particles[i].outofbounds || particles[i].ttl == 0) + continue; + + // generate RGB values for particle + if (firemode) + { + //TODO: decide on a final version... + //brightness = (uint32_t)particles[i].ttl * (1 + (fireintensity >> 4)) + (fireintensity >> 2); //this is good + //brightness = (uint32_t)particles[i].ttl * (fireintensity >> 3) + (fireintensity >> 1); // this is experimental, also works, flamecolor is more even, does not look as good (but less puffy at lower speeds) + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (uint32_t)particles[i].ttl * (fireintensity >> 4) + (fireintensity >> 1); // this is experimental //multiplikation mit weniger als >>4 macht noch mehr puffs bei low speed + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + particles[i].ttl + (fireintensity>>1); // this is experimental + //brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + ((particles[i].ttl * fireintensity) >> 5); // this is experimental TODO: test this -> testing... ok but not the best, bit sparky + brightness = (((uint32_t)particles[i].ttl * (maxY + PS_P_RADIUS - particles[i].y)) >> 7) + (fireintensity >> 1); // this is experimental TODO: test this -> testing... does not look too bad! + brightness = brightness > 255 ? 255 : brightness; // faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, brightness, 255, LINEARBLEND); + } + else{ + brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + if (particles[i].sat < 255) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = particles[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB + } + } + + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + } + + if (particlesize > 0) + { + uint32_t passes = particlesize/64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + + for(int i = 0; i < passes; i++) + { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (useLocalBuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else + SEGMENT.blur(bluramount << bitshift, true); + bluramount -= 64; + } + } + + if (useLocalBuffer) // transfer local buffer back to segment + { + uint32_t yflipped; + for (int y = 0; y <= maxYpixel; y++) + { + yflipped = maxYpixel - y; + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); + } + } + free(framebuffer); + } + if (renderbuffer) + free(renderbuffer); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) { - int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work - int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs - bool advancedrender = false; // rendering for advanced particles - - // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient - int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; - int32_t yoffset = particles[particleindex].y - PS_P_HALFRADIUS; - int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space - int32_t dy = yoffset % PS_P_RADIUS; - int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) - int32_t y = yoffset >> PS_P_RADIUS_SHIFT; - - // check if particle has advanced size properties and buffer is available - if(advPartProps) - { - if(advPartProps[particleindex].size > 0) - { - if(renderbuffer) - { - advancedrender = true; - memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels - } - else - return; // cannot render without buffer, advanced size particles are allowed out of frame - } - } - - // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixco[0][0] = pixco[3][0] = x; // bottom left & top left - pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right - pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right - pixco[2][1] = pixco[3][1] = y + 1; // top right & top left - - // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) - if (x < 0) // left pixels out of frame - { - dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) - // note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) - // checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: - if (dx == PS_P_RADIUS) - { - pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render - } - if (particlesettings.wrapX) // wrap x to the other side if required - pixco[0][0] = pixco[3][0] = maxXpixel; - else - pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render - } - else if (pixco[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow - { - if (particlesettings.wrapX) // wrap y to the other side if required - pixco[1][0] = pixco[2][0] = 0; - else - pxlbrightness[1] = pxlbrightness[2] = -1; - } - - if (y < 0) // bottom pixels out of frame - { - dy = PS_P_RADIUS + dy; //see note above - if (dy == PS_P_RADIUS) - { - pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render - } - if (particlesettings.wrapY) // wrap y to the other side if required - pixco[0][1] = pixco[1][1] = maxYpixel; - else - pxlbrightness[0] = pxlbrightness[1] = -1; - } - else if (pixco[2][1] > maxYpixel) // top pixels - { - if (particlesettings.wrapY) // wrap y to the other side if required - pixco[2][1] = pixco[3][1] = 0; - else - pxlbrightness[2] = pxlbrightness[3] = -1; - } - - if(advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) - { - for(uint32_t i = 0; i < 4; i++) - pxlbrightness[i] = 0; - } - - // calculate brightness values for all four pixels representing a particle using linear interpolation - // precalculate values for speed optimization - int32_t precal1 = (int32_t)PS_P_RADIUS - dx; - int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess; - int32_t precal3 = dy * brightess; - - //calculate the values for pixels that are in frame - if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE - if (pxlbrightness[2] >= 0) - pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE - if (pxlbrightness[3] >= 0) - pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE - - if(advancedrender) - { - //render particle to a bigger size - //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 - //first, render the pixel to the center of the renderbuffer, then apply 2D blurring - fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left - fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); - fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); - fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... - uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 - uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) - uint32_t maxsize = advPartProps[particleindex].size; - uint32_t xsize = maxsize; - uint32_t ysize = maxsize; - if(advPartSize) // use advanced size control - { - if(advPartSize[particleindex].asymmetry > 0) - getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); - maxsize = xsize; - if(ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two - } - maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max - uint32_t bitshift = 0; - for(int i = 0; i < maxsize; i++) - { - if(i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) - bitshift = 1; - rendersize += 2; - offset--; - blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 - xsize = xsize > 64 ? xsize - 64 : 0; - ysize = ysize > 64 ? ysize - 64 : 0; - } - - // calculate origin coordinates to render the particle to in the framebuffer - uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; - uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked - - // transfer particle renderbuffer to framebuffer - for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) - { - xfb = xfb_orig + xrb; - if(xfb > maxXpixel) - { - if (particlesettings.wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); - else - continue; - } - - for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) - { - yfb = yfb_orig + yrb; - if(yfb > maxYpixel) - { - if (particlesettings.wrapY) // wrap y to the other side if required - yfb = yfb % (maxYpixel + 1); - else - continue; - } - fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); - } - } - } - else // standard rendering - { - if (framebuffer) - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left - } - } - else - { - for(uint32_t i = 0; i < 4; i++) - { - if (pxlbrightness[i] > 0) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); - } - } - } + int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool advancedrender = false; // rendering for advanced particles + + // subtract half a radius as the rendering algorithm always starts at the bottom left, this makes calculations more efficient + int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y - PS_P_HALFRADIUS; + int32_t dx = xoffset % PS_P_RADIUS; //relativ particle position in subpixel space + int32_t dy = yoffset % PS_P_RADIUS; + int32_t x = xoffset >> PS_P_RADIUS_SHIFT; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + int32_t y = yoffset >> PS_P_RADIUS_SHIFT; + + // check if particle has advanced size properties and buffer is available + if (advPartProps) + { + if (advPartProps[particleindex].size > 0) + { + if (renderbuffer) + { + advancedrender = true; + memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels + } + else + return; // cannot render without buffer, advanced size particles are allowed out of frame + } + } + + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[1][0] = pixco[2][0] = x + 1; // bottom right & top right + pixco[2][1] = pixco[3][1] = y + 1; // top right & top left + + // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) + if (x < 0) // left pixels out of frame + { + dx = PS_P_RADIUS + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value (really old bug now finally fixed) + // note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) + // checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: + if (dx == PS_P_RADIUS) + { + pxlbrightness[1] = pxlbrightness[2] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapX) // wrap x to the other side if required + pixco[0][0] = pixco[3][0] = maxXpixel; + else + pxlbrightness[0] = pxlbrightness[3] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1][0] > maxXpixel) // right pixels, only has to be checkt if left pixels did not overflow + { + if (particlesettings.wrapX) // wrap y to the other side if required + pixco[1][0] = pixco[2][0] = 0; + else + pxlbrightness[1] = pxlbrightness[2] = -1; + } + + if (y < 0) // bottom pixels out of frame + { + dy = PS_P_RADIUS + dy; //see note above + if (dy == PS_P_RADIUS) + { + pxlbrightness[2] = pxlbrightness[3] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapY) // wrap y to the other side if required + pixco[0][1] = pixco[1][1] = maxYpixel; + else + pxlbrightness[0] = pxlbrightness[1] = -1; + } + else if (pixco[2][1] > maxYpixel) // top pixels + { + if (particlesettings.wrapY) // wrap y to the other side if required + pixco[2][1] = pixco[3][1] = 0; + else + pxlbrightness[2] = pxlbrightness[3] = -1; + } + + if (advancedrender) // always render full particles in advanced rendering, undo out of frame marking (faster than checking each time in code above) + { + for(uint32_t i = 0; i < 4; i++) + pxlbrightness[i] = 0; + } + + // calculate brightness values for all four pixels representing a particle using linear interpolation + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess; + int32_t precal3 = dy * brightess; + + //calculate the values for pixels that are in frame + if (pxlbrightness[0] >= 0) + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + if (pxlbrightness[2] >= 0) + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + if (pxlbrightness[3] >= 0) + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + + if (advancedrender) + { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4][4], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5][4], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5][5], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4][5], color, pxlbrightness[3]); //TODO: make this a loop somehow? needs better coordinate handling... + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) // use advanced size control + { + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = xsize; + if (ysize > maxsize) maxsize = ysize; //maxsize is now the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for(int i = 0; i < maxsize; i++) + { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if (xfb > maxXpixel) + { + if (particlesettings.wrapX) // wrap x to the other side if required + xfb = xfb % (maxXpixel + 1); + else + continue; + } + + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) + { + yfb = yfb_orig + yrb; + if (yfb > maxYpixel) + { + if (particlesettings.wrapY) // wrap y to the other side if required + yfb = yfb % (maxYpixel + 1); + else + continue; + } + fast_color_add(framebuffer[xfb][yfb], renderbuffer[xrb][yrb]); + } + } + } + else // standard rendering + { + if (framebuffer) + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i][0]][pixco[i][1]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 4; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } /* - // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) - for (uint32_t d = 0; d < 4; d++) - { - if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) - { - //Serial.print("<"); - if (pxlbrightness[d] >= 0) - { - Serial.print("uncought out of bounds: x:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) - { - //Serial.print("^"); - if (pxlbrightness[d] >= 0) - { - Serial.print("uncought out of bounds: y:"); - Serial.print(pixco[d][0]); - Serial.print(" y:"); - Serial.print(pixco[d][1]); - Serial.print("particle x="); - Serial.print(particles[particleindex].x); - Serial.print(" y="); - Serial.println(particles[particleindex].y); - pxlbrightness[d] = -1; // do not render - } - } - } + // debug: check coordinates if out of buffer boundaries print out some info (rendering out of bounds particle causes crash!) + for (uint32_t d = 0; d < 4; d++) + { + if (pixco[d][0] < 0 || pixco[d][0] > maxXpixel) + { + //Serial.print("<"); + if (pxlbrightness[d] >= 0) + { + Serial.print("uncought out of bounds: x:"); + Serial.print(pixco[d][0]); + Serial.print(" y:"); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[particleindex].x); + Serial.print(" y="); + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render + } + } + if (pixco[d][1] < 0 || pixco[d][1] > maxYpixel) + { + //Serial.print("^"); + if (pxlbrightness[d] >= 0) + { + Serial.print("uncought out of bounds: y:"); + Serial.print(pixco[d][0]); + Serial.print(" y:"); + Serial.print(pixco[d][1]); + Serial.print("particle x="); + Serial.print(particles[particleindex].x); + Serial.print(" y="); + Serial.println(particles[particleindex].y); + pxlbrightness[d] = -1; // do not render + } + } + } */ } @@ -1060,252 +1061,252 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // particles move upwards faster if ttl is high (i.e. they are hotter) void ParticleSystem::fireParticleupdate() { - //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function - uint32_t i = 0; - - for (i = 0; i < usedParticles; i++) - { - if (particles[i].ttl > 0) - { - // age - particles[i].ttl--; - // apply velocity - particles[i].x = particles[i].x + (int32_t)particles[i].vx; - particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter - //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting - particles[i].outofbounds = 0; - // check if particle is out of bounds, wrap x around to other side if wrapping is enabled - // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds - // y-direction - if (particles[i].y < -PS_P_HALFRADIUS) - particles[i].outofbounds = 1; - else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top - particles[i].ttl = 0; - else // particle is in frame in y direction, also check x direction now - { - if ((particles[i].x < 0) || (particles[i].x > maxX)) - { - if (particlesettings.wrapX) - { - particles[i].x = (uint16_t)particles[i].x % (maxX + 1); - } - else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view - { - particles[i].ttl = 0; - } - } - } - } - } + //TODO: cleanup this function? check if normal move is much slower, change move function to check y first then this function just needs to add ttl to y befor calling normal move function + uint32_t i = 0; + + for (i = 0; i < usedParticles; i++) + { + if (particles[i].ttl > 0) + { + // age + particles[i].ttl--; + // apply velocity + particles[i].x = particles[i].x + (int32_t)particles[i].vx; + particles[i].y = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + //particles[i].y = particles[i].y + (int32_t)particles[i].vy;// + (particles[i].ttl >> 3); // younger particles move faster upward as they are hotter //this is experimental, different shifting + particles[i].outofbounds = 0; + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds + // y-direction + if (particles[i].y < -PS_P_HALFRADIUS) + particles[i].outofbounds = 1; + else if (particles[i].y > maxY + PS_P_HALFRADIUS) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now + { + if ((particles[i].x < 0) || (particles[i].x > maxX)) + { + if (particlesettings.wrapX) + { + particles[i].x = (uint16_t)particles[i].x % (maxX + 1); + } + else if ((particles[i].x < -PS_P_HALFRADIUS) || (particles[i].x > maxX + PS_P_HALFRADIUS)) //if fully out of view + { + particles[i].ttl = 0; + } + } + } + } + } } // detect collisions in an array of particles and handle them void ParticleSystem::handleCollisions() { - // detect and handle collisions - uint32_t i, j; - uint32_t startparticle = 0; - uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up - // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) - // if more accurate collisions are needed, just call it twice in a row - if (collisioncounter & 0x01) - { - startparticle = endparticle; - endparticle = usedParticles; - } - collisioncounter++; - - for (i = startparticle; i < endparticle; i++) - { - // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide - if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view - { - int32_t dx, dy; // distance to other particles - for (j = i + 1; j < usedParticles; j++) // check against higher number particles - { - if (particles[j].ttl > 0) // if target particle is alive - { - dx = particles[i].x - particles[j].x; - if(advPartProps) //may be using individual particle size - { - particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance - } - if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction - { - dy = particles[i].y - particles[j].y; - if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close - collideParticles(&particles[i], &particles[j]); - } - } - } - } - } + // detect and handle collisions + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = usedParticles >> 1; // do half the particles, significantly speeds things up + // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // if more accurate collisions are needed, just call it twice in a row + if (collisioncounter & 0x01) + { + startparticle = endparticle; + endparticle = usedParticles; + } + collisioncounter++; + + for (i = startparticle; i < endparticle; i++) + { + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view + { + int32_t dx, dy; // distance to other particles + for (j = i + 1; j < usedParticles; j++) // check against higher number particles + { + if (particles[j].ttl > 0) // if target particle is alive + { + dx = particles[i].x - particles[j].x; + if (advPartProps) //may be using individual particle size + { + particleHardRadius = PS_P_MINHARDRADIUS + particlesize + (((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1); // collision distance + } + if (dx < particleHardRadius && dx > -particleHardRadius) // check x direction, if close, check y direction + { + dy = particles[i].y - particles[j].y; + if (dy < particleHardRadius && dy > -particleHardRadius) // particles are close + collideParticles(&particles[i], &particles[j]); + } + } + } + } + } } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particle2) // TODO: dx,dy is calculated just above, can pass it over here to save a few CPU cycles? { - int32_t dx = particle2->x - particle1->x; - int32_t dy = particle2->y - particle1->y; - int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) - int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; - int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; - - // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) - if (distanceSquared == 0) - { - // Adjust positions based on relative velocity direction - dx = -1; - if (relativeVx < 0) // if true, particle2 is on the right side - dx = 1; - else if(relativeVx == 0) - relativeVx = 1; - - dy = -1; - if (relativeVy < 0) - dy = 1; - else if (relativeVy == 0) - relativeVy = 1; - - distanceSquared = 2; //1 + 1 - } - - // Calculate dot product of relative velocity and relative distance - int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other - int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number - - if (dotProduct < 0) // particles are moving towards each other - { - // integer math used to avoid floats. - // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen - // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers - // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value - int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) - int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift - int32_t yimpulse = ((impulse) * dy) / 32767; - particle1->vx += ximpulse; - particle1->vy += yimpulse; - particle2->vx -= ximpulse; - particle2->vy -= yimpulse; - - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) - { - const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); - particle1->vx = ((int32_t)particle1->vx * coeff) / 255; - particle1->vy = ((int32_t)particle1->vy * coeff) / 255; - - particle2->vx = ((int32_t)particle2->vx * coeff) / 255; - particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - - if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other - { - particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; - particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; - - particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; - particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; - } - } - - // particles have volume, push particles apart if they are too close - // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way - // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. - { - int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are - int32_t push = 0; - if (dx < 0) // particle 1 is on the right - push = pushamount; - else if (dx > 0) - push = -pushamount; - else // on the same x coordinate, shift it a little so they do not stack - { - if (notsorandom) - particle1->x++; // move it so pile collapses - else - particle1->x--; - } - particle1->vx += push; - push = 0; - if (dy < 0) - push = pushamount; - else if (dy > 0) - push = -pushamount; - else // dy==0 - { - if (notsorandom) - particle1->y++; // move it so pile collapses - else - particle1->y--; - } - particle1->vy += push; - // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye - } - } + int32_t dx = particle2->x - particle1->x; + int32_t dy = particle2->y - particle1->y; + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + int32_t relativeVx = (int16_t)particle2->vx - (int16_t)particle1->vx; + int32_t relativeVy = (int16_t)particle2->vy - (int16_t)particle1->vy; + + // if dx and dy are zero (i.e. they meet at the center) give them an offset, if speeds are also zero, also offset them (pushes them apart if they are clumped before enabling collisions) + if (distanceSquared == 0) + { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; //1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + + if (dotProduct < 0) // particles are moving towards each other + { + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; + particle1->vx += ximpulse; + particle1->vy += yimpulse; + particle2->vx -= ximpulse; + particle2->vy -= yimpulse; + + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) + { + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle1->vy = ((int32_t)particle1->vy * coeff) / 255; + + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + particle2->vy = ((int32_t)particle2->vy * coeff) / 255; + + if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; + particle1->vy = (particle1->vy < 3 && particle1->vy > -3) ? 0 : particle1->vy; + + particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; + particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; + } + } + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if (dotProduct > -250) //this means particles are slow (or really really close) so push them apart. + { + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else // on the same x coordinate, shift it a little so they do not stack + { + if (notsorandom) + particle1->x++; // move it so pile collapses + else + particle1->x--; + } + particle1->vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else // dy==0 + { + if (notsorandom) + particle1->y++; // move it so pile collapses + else + particle1->y--; + } + particle1->vy += push; + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + } + } } // calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) // force is in 3.4 fixedpoint notation, +/-127 int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) { - if(force == 0) - return 0; - // for small forces, need to use a delay counter - int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) - int32_t dv; - // for small forces, need to use a delay counter, apply force only if it overflows - if (force_abs < 16) - { - *counter += force_abs; - if (*counter > 15) - { - *counter -= 16; - dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) - } - } - else - { - dv = force >> 4; // MSBs - } - return dv; + if (force == 0) + return 0; + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) + { + *counter += force_abs; + if (*counter > 15) + { + *counter -= 16; + dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + } + } + else + { + dv = force >> 4; // MSBs + } + return dv; } // limit speed to prevent overflows int32_t ParticleSystem::limitSpeed(int32_t speed) { - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); } // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) -{ - CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); - if (array2D == NULL) - DEBUG_PRINT(F("PS buffer alloc failed")); - else - { - // assign pointers of 2D array - CRGB *start = (CRGB *)(array2D + cols); - for (uint i = 0; i < cols; i++) - { - array2D[i] = start + i * rows; - } - } - return array2D; +{ + CRGB ** array2D = (CRGB **)calloc(cols, sizeof(CRGB *) + rows * sizeof(CRGB)); + if (array2D == NULL) + DEBUG_PRINT(F("PS buffer alloc failed")); + else + { + // assign pointers of 2D array + CRGB *start = (CRGB *)(array2D + cols); + for (uint i = 0; i < cols; i++) + { + array2D[i] = start + i * rows; + } + } + return array2D; } // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) void ParticleSystem::updateSystem(void) { - // update matrix size - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - setMatrixSize(cols, rows); - updatePSpointers(advPartProps != NULL, advPartSize != NULL); + // update matrix size + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + setMatrixSize(cols, rows); + updatePSpointers(advPartProps != NULL, advPartSize != NULL); } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) @@ -1313,124 +1314,124 @@ void ParticleSystem::updateSystem(void) // FX handles the PSsources, need to tell this function how many there are void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) { - // DEBUG_PRINT(F("*** PS pointers ***")); - // DEBUG_PRINTF_P(PSTR("this PS %p "), this); - // Note on memory alignment: - // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. - // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. - // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) - if(isadvanced) - { - advPartProps = reinterpret_cast(sources + numSources); - PSdataEnd = reinterpret_cast(advPartProps + numParticles); - if(sizecontrol) - { - advPartSize = reinterpret_cast(advPartProps + numParticles); - PSdataEnd = reinterpret_cast(advPartSize + numParticles); - } - } - else - { - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data - } - /* - DEBUG_PRINTF_P(PSTR(" particles %p "), particles); - DEBUG_PRINTF_P(PSTR(" sources %p "), sources); - DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); - DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); - DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); - */ + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + if (isadvanced) + { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) + { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } + else + { + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + } + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ } //non class functions to use for initialization uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel - uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) + uint32_t numberofParticles = (cols * rows * 3) / 4; // 0.75 particle per pixel + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) #elif ARDUINO_ARCH_ESP32S2 - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel - uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) #else - uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) - uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) + uint32_t numberofParticles = (cols * rows); // 1 particle per pixel (for example 512 particles on 32x16) + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) #endif - numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); - if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); - if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount - numberofParticles /= 8; // if size control is used, much fewer particles are needed - - //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; - return numberofParticles; + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles /= 8; // if size control is used, much fewer particles are needed + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; + return numberofParticles; } uint32_t calculateNumberOfSources(uint8_t requestedsources) { - uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); #ifdef ESP8266 - int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 + int numberofSources = min((cols * rows) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 + int numberofSources = min((cols * rows) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 #else - int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 + int numberofSources = min((cols * rows) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 #endif - // make sure it is a multiple of 4 for proper memory alignment - numberofSources = ((numberofSources+3) >> 2) << 2; - return numberofSources; + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { - uint32_t requiredmemory = sizeof(ParticleSystem); - // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) - requiredmemory += sizeof(PSparticle) * numparticles; - if (isadvanced) - requiredmemory += sizeof(PSadvancedParticle) * numparticles; - if (sizecontrol) - requiredmemory += sizeof(PSsizeControl) * numparticles; - requiredmemory += sizeof(PSsource) * numsources; - requiredmemory += additionalbytes; - //Serial.print("allocating: "); - //Serial.print(requiredmemory); - //Serial.println("Bytes"); - //Serial.print("allocating for segment at"); - //Serial.println((uintptr_t)SEGMENT.data); - return(SEGMENT.allocateData(requiredmemory)); + uint32_t requiredmemory = sizeof(ParticleSystem); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticle) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); + return(SEGMENT.allocateData(requiredmemory)); } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) { - //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); - uint32_t numsources = calculateNumberOfSources(requestedsources); - //Serial.print("numsources: "); - //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) - { - DEBUG_PRINT(F("PS init failed: memory depleted")); - return false; - } - //Serial.print("segment.data ptr"); - //Serial.println((uintptr_t)(SEGMENT.data)); - uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor - //Serial.print("PS pointer at "); - //Serial.println((uintptr_t)PartSys); - return true; + //Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); + uint32_t numsources = calculateNumberOfSources(requestedsources); + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); + uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; + uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + //Serial.println("calling constructor"); + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor + //Serial.print("PS pointer at "); + //Serial.println((uintptr_t)PartSys); + return true; } /////////////////////// @@ -1443,43 +1444,42 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint // note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) { - uint32_t r, g, b; - if(scale < 255) - { - r = c1.r + ((c2.r * scale) >> 8); - g = c1.g + ((c2.g * scale) >> 8); - b = c1.b + ((c2.b * scale) >> 8); - } - else{ - r = c1.r + c2.r; - g = c1.g + c2.g; - b = c1.b + c2.b; - } - uint32_t max = r; - if (g > max) // note: using ? operator would be slower by 2 instructions - max = g; - if (b > max) - max = b; - if (max < 256) - { - c1.r = r; // save result to c1 - c1.g = g; - c1.b = b; - } - else - { - c1.r = (r * 255) / max; - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; - } + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + uint32_t max = r; + if (g > max) // note: using ? operator would be slower by 2 instructions + max = g; + if (b > max) + max = b; + if (max < 256) + { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } + else + { + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; + } } // faster than fastled color scaling as it uses a 32bit scale factor and pointer void fast_color_scale(CRGB &c, uint32_t scale) { - c.r = ((c.r * scale) >> 8); - c.g = ((c.g * scale) >> 8); - c.b = ((c.b * scale) >> 8); + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); } // blur a matrix in x and y direction, blur can be asymmetric in x and y @@ -1487,57 +1487,61 @@ void fast_color_scale(CRGB &c, uint32_t scale) // to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) { - CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - if(isparticle) //first and last row are always black in particle rendering - { - ystart++; - ysize--; - } - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - carryover = BLACK; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if(!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); - - if(x > 0) - { - fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel - } - - if(isparticle) // now also do first and last row - { - ystart--; - ysize++; - } - - seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - carryover = BLACK; - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if(!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - yblur); - - if(y > 0) - { - fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; - } - fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel - } -} \ No newline at end of file + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + if (isparticle) //first and last row are always black in particle rendering + { + ystart++; + ysize--; + } + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + carryover = BLACK; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + if (isparticle) // now also do first and last row + { + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + carryover = BLACK; + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - yblur); + + if (y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } +} + +#endif // WLED_DISABLE_PARTICLESYSTEM + + diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 281afee228..2eafe40893 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -22,51 +22,51 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ +#ifndef WLED_DISABLE_PARTICLESYSTEM #include #include "FastLED.h" -//memory allocation -#define ESP8266_MAXPARTICLES 180// // enough for one 16x16 segment with transitions +// memory allocation +#define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 #define ESP32S2_MAXPARTICLES 840 // enough for four 16x16 segments #define ESP32S2_MAXSOURCES 48 -#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... +#define ESP32_MAXPARTICLES 1024 // enough for four 16x16 segments TODO: not enough for one 64x64 panel... #define ESP32_MAXSOURCES 64 -//particle dimensions (subpixel division) -#define PS_P_RADIUS 64 //subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) -#define PS_P_HALFRADIUS 32 +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS 32 #define PS_P_RADIUS_SHIFT 6 // shift for RADIUS #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_MINHARDRADIUS 70 // minimum hard surface radius -#define PS_P_MINSURFACEHARDNESS 128 //minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 120 //maximum speed a particle can have (vx/vy is int8) +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) //struct for a single particle (10 bytes) typedef struct { - int16_t x; //x position in particle system - int16_t y; //y position in particle system - int8_t vx; //horizontal velocity - int8_t vy; //vertical velocity + int16_t x; // x position in particle system + int16_t y; // y position in particle system + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity uint8_t hue; // color hue - uint8_t sat; //particle color saturation - //two byte bit field: + uint8_t sat; // particle color saturation + // two byte bit field: uint16_t ttl : 12; // time to live, 12 bit or 4095 max (which is 50s at 80FPS) - bool outofbounds : 1; //out of bounds flag, set to true if particle is outside of display area - bool collide : 1; //if set, particle takes part in collisions - bool perpetual : 1; //if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool flag4 : 1; // unused flag + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool flag4 : 1; // unused flag } PSparticle; // struct for additional particle settings (optional) typedef struct { - uint8_t size; //particle size, 255 means 10 pixels in diameter - uint8_t forcecounter; //counter for applying forces to individual particles + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; // struct for advanced particle size control (optional) @@ -74,24 +74,24 @@ typedef struct { uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) - uint8_t maxsize; // target size for growing - uint8_t minsize; // target size for shrinking + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) uint8_t wobblecounter : 4; - uint8_t growspeed : 4; - uint8_t shrinkspeed : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; uint8_t wobblespeed : 4; bool grow : 1; // flags bool shrink : 1; - bool pulsate : 1; //grows & shrinks & grows & ... - bool wobble : 1; //alternate x and y size + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size } PSsizeControl; //struct for a particle source (17 bytes) typedef struct { - uint16_t minLife; // minimum ttl of emittet particles - uint16_t maxLife; // maximum ttl of emitted particles + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles PSparticle source; // use a particle as the emitter source (speed, position, color) uint8_t var; // variation of emitted speed (use odd numbers for good symmetry) int8_t vx; // emitting speed @@ -103,20 +103,20 @@ typedef struct { typedef union { struct{ - // add a one byte bit field: + // one byte bit field: bool wrapX : 1; bool wrapY : 1; bool bounceX : 1; bool bounceY : 1; bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; //set to 1 if gravity is used, disables bounceY at the top + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top bool useCollisions : 1; bool colorByAge : 1; // if set, particle hue is set by ttl value in render function }; - byte asByte; //order is: LSB is first entry in the list above + byte asByte; // order is: LSB is first entry in the list above } PSsettings; -//class uses approximately 60 bytes +// class uses approximately 60 bytes class ParticleSystem { public: @@ -129,49 +129,49 @@ class ParticleSystem // particle emitters int32_t sprayEmit(PSsource &emitter); void flameEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + - // move functions - void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); - //particle physics + void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); - void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); //use this for advanced property particles - void applyForce(int8_t xforce, int8_t yforce); //apply a force to all particles + void applyForce(uint16_t particleindex, int8_t xforce, int8_t yforce); // use this for advanced property particles + void applyForce(int8_t xforce, int8_t yforce); // apply a force to all particles void applyAngleForce(PSparticle *part, int8_t force, uint16_t angle, uint8_t *counter); void applyAngleForce(uint16_t particleindex, int8_t force, uint16_t angle); // use this for advanced property particles - void applyAngleForce(int8_t force, uint16_t angle); //apply angular force to all particles + void applyAngleForce(int8_t force, uint16_t angle); // apply angular force to all particles void applyFriction(PSparticle *part, int32_t coefficient); // apply friction to specific particle void applyFriction(int32_t coefficient); // apply friction to all used particles void pointAttractor(uint16_t particleindex, PSparticle *attractor, uint8_t strength, bool swallow); void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); - //set options + // set options void setUsedParticles(uint16_t num); - void setCollisionHardness(uint8_t hardness); //hardness for particle collisions (255 means full hard) - void setWallHardness(uint8_t hardness); //hardness for bouncing on the wall if bounceXY is set - void setWallRoughness(uint8_t roughness); //wall roughness randomizes wall collisions + void setCollisionHardness(uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions void setMatrixSize(uint16_t x, uint16_t y); void setWrapX(bool enable); void setWrapY(bool enable); void setBounceX(bool enable); void setBounceY(bool enable); - void setKillOutOfBounds(bool enable); //if enabled, particles outside of matrix instantly die - void setSaturation(uint8_t sat); //set global color saturation + void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(uint8_t sat); // set global color saturation void setColorByAge(bool enable); - void setMotionBlur(uint8_t bluramount); //note: motion blur can only be used if 'particlesize' is set to zero + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setParticleSize(uint8_t size); void setGravity(int8_t force = 8); - void enableParticleCollisions(bool enable, uint8_t hardness = 255); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); PSparticle *particles; // pointer to particle array PSsource *sources; // pointer to sources PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) - PSsizeControl *advPartSize; //pointer to advanced particle size control (can be NULL) - uint8_t* PSdataEnd; //points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint16_t maxX, maxY; //particle system size i.e. width-1 / height-1 in subpixels + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + uint16_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 - uint8_t numSources; //number of sources + uint8_t numSources; // number of sources uint16_t numParticles; // number of particles available in this system uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) @@ -188,9 +188,9 @@ class ParticleSystem //utility functions void updatePSpointers(bool isadvanced, bool sizecontrol); // update the data pointers to current segment data space - void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); //advanced size control + void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); - void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); //bounce on a wall + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); int32_t calcForce_dv(int8_t force, uint8_t *counter); int32_t limitSpeed(int32_t speed); @@ -202,22 +202,24 @@ class ParticleSystem int32_t collisionHardness; uint8_t wallHardness; uint8_t wallRoughness; - uint8_t gforcecounter; //counter for global gravity - int8_t gforce; //gravity strength, default is 8 (negative is allowed, positive is downwards) - uint8_t collisioncounter; //counter to handle collisions TODO: could use the SEGMENT.call? - uint8_t forcecounter; //counter for globally applied forces - //global particle properties for basic particles - uint8_t particlesize; //global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint8_t forcecounter; // counter for globally applied forces + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection - uint8_t motionBlur; //enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -//initialization functions (not part of class) +// initialization functions (not part of class) bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); uint32_t calculateNumberOfSources(uint8_t requestedsources); bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); -//color add function +// color functions void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -void fast_color_scale(CRGB &c, uint32_t scale); //fast scaling function using 32bit factor (keep it 0-255) and pointer -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); \ No newline at end of file +void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); + +#endif // WLED_DISABLE_PARTICLESYSTEM From 3c5928f8b2792a9dfec18f7c3ff53a714ef1c869 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 May 2024 14:19:31 +0200 Subject: [PATCH 16/21] Added PS source option to emit more than one particle, addes AR to Blobs and Spray --- wled00/FX.cpp | 66 ++++++++++++++++++++++++++++++------- wled00/FXparticleSystem.cpp | 44 +++++++++++++------------ wled00/FXparticleSystem.h | 6 ++-- 3 files changed, 81 insertions(+), 35 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 22d9ede777..b620d242f3 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9164,11 +9164,9 @@ uint16_t mode_particlespray(void) if (!initParticleSystem(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) - PartSys->setBounceY(true); + PartSys->setBounceY(true); PartSys->setMotionBlur(200); // anable motion blur PartSys->sources[0].source.hue = random16(); - PartSys->sources[0].maxLife = 300; // lifetime in frames - PartSys->sources[0].minLife = 100; PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].var = 3; @@ -9195,21 +9193,55 @@ uint16_t mode_particlespray(void) else PartSys->enableParticleCollisions(false); + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) // defines interval of particle emit + { + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + volumeRaw >> 4; + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeSmth >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else{ //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles + { + PartSys->sources[0].maxLife = 300; // lifetime in frames + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + #else // change source properties if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) // every nth frame, cycle color and emit particles { - PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source - // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) - PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); - PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); - // spray[j].source.hue = random16(); //set random color for each particle (using palette) - PartSys->angleEmit(PartSys->sources[0], (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8, SEGMENT.speed >> 2); + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); } + #endif PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collisions;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; /* @@ -9464,12 +9496,24 @@ uint16_t mode_particleblobs(void) SEGMENT.aux0 = SEGMENT.speed; //write state back SEGMENT.aux1 = SEGMENT.custom1; + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + if(SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + #endif PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; /* diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index 4787a4d7f2..f267796b41 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -33,7 +33,6 @@ /* TODO: -add function to 'update sources' so FX does not have to take care of that. FX can still implement its own version if so desired. - -add possiblity to emit more than one particle, just pass a source and the amount to emit or even add several sources and the amount, function decides if it should do it fair or not -add an x/y struct, do particle rendering using that, much easier to read */ @@ -217,27 +216,30 @@ void ParticleSystem::enableParticleCollisions(bool enable, uint8_t hardness) // collisionHardness = hardness + 1; } -// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) -int32_t ParticleSystem::sprayEmit(PSsource &emitter) +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem::sprayEmit(PSsource &emitter, uint32_t amount) { - for (int32_t i = 0; i < usedParticles; i++) + for (uint32_t a = 0; a < amount; a++) { - emitIndex++; - if (emitIndex >= usedParticles) - emitIndex = 0; - if (particles[emitIndex].ttl == 0) // find a dead particle + for (int32_t i = 0; i < usedParticles; i++) { - particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); - particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); - particles[emitIndex].x = emitter.source.x; - particles[emitIndex].y = emitter.source.y; - particles[emitIndex].hue = emitter.source.hue; - particles[emitIndex].sat = emitter.source.sat; - particles[emitIndex].collide = emitter.source.collide; - particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); - if (advPartProps) - advPartProps[emitIndex].size = emitter.size; - return i; + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.vx + random(-emitter.var, emitter.var); + particles[emitIndex].vy = emitter.vy + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].ttl = random(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + return i; + } } } return -1; @@ -273,11 +275,11 @@ void ParticleSystem::flameEmit(PSsource &emitter) // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var // angle = 0 means in positive x-direction (i.e. to the right) -void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed) +void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, uint32_t amount) { emitter.vx = ((int32_t)cos16(angle) * (int32_t)speed) / (int32_t)32600; // cos16() and sin16() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding emitter.vy = ((int32_t)sin16(angle) * (int32_t)speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! - sprayEmit(emitter); + sprayEmit(emitter, amount); } // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 2eafe40893..57a54e5406 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -93,7 +93,7 @@ typedef struct { uint16_t minLife; // minimum ttl of emittet particles uint16_t maxLife; // maximum ttl of emitted particles PSparticle source; // use a particle as the emitter source (speed, position, color) - uint8_t var; // variation of emitted speed (use odd numbers for good symmetry) + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed int8_t vy; uint8_t size; // particle size (advanced property) @@ -127,9 +127,9 @@ class ParticleSystem void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions // particle emitters - int32_t sprayEmit(PSsource &emitter); + int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); - void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed); + void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function From 21e4604424dda3cd8db9c44ed2c90ce3831cb80f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 May 2024 21:35:12 +0200 Subject: [PATCH 17/21] added center GEQ back in, changed AR behaviour of spray a little center GEQ is a mix between the PS equalizer and vortex, tuned for AR. some more tuning may be needed, it can probably be extended and improved a little. --- wled00/FX.cpp | 228 ++++++++++++++++++++------------------------------ 1 file changed, 92 insertions(+), 136 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b620d242f3..857cc1cce1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9209,8 +9209,8 @@ uint16_t mode_particlespray(void) if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) // defines interval of particle emit { PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames - PartSys->sources[0].var = 1 + volumeRaw >> 4; - uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeSmth >> 3); + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); PartSys->sources[0].source.hue += volumeSmth/30; PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); } @@ -9340,6 +9340,95 @@ uint16_t mode_particleGEQ(void) } static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS Equalizer@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0,c2=128,o1=0,o2=0,o3=0"; +/* + * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) + * Particles sprayed from center with a rotating spray + * Uses palette for particle color + * by DedeHai (Damian Schneider) + */ + +#define NUMBEROFSOURCES 16 +uint16_t mode_particlecenterGEQ(void) +{ +if (SEGLEN == 1) + return mode_static(); + + ParticleSystem *PartSys = NULL; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) + { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i*16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + + if (SEGMENT.check2) + SEGMENT.aux0 += SEGMENT.custom1 << 2; + else + SEGMENT.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = random(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < numSprays; i++) + { + if(SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + PartSys->sources[j].var = SEGMENT.custom3>>1; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGMENT.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + { + emitparticles = 1; + } + else if (fftResult[j] > 0) // band has low value + { + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (random16() % restvolume == 0) + { + emitparticles = 1; + } + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + j = (j + 1) % numSprays; + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;012;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; + /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) */ @@ -9515,139 +9604,6 @@ uint16_t mode_particleblobs(void) } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; - -/* - * Particle rotating GEQ (unfinished, basically works but needs more fine-tuning) - * Particles sprayed from center with a rotating spray - * Uses palette for particle color - * by DedeHai (Damian Schneider) - */ -/* -uint16_t mode_particlecenterGEQ(void) -{ - - if (SEGLEN == 1) - return mode_static(); - - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); - -#ifdef ESP8266 - const uint32_t numParticles = 50; // maximum number of particles -#else - const uint32_t numParticles = 500; // maximum number of particles -#endif - - const uint8_t numSprays = 16; // maximum number of sprays - - PSparticle *particles; - PSsource *spray; - - // allocate memory and divide it into proper pointers, max is 32kB for all segments, 100 particles use 1200bytes - uint32_t dataSize = sizeof(PSparticle) * numParticles; - dataSize += sizeof(PSsource) * (numSprays); - if (!SEGMENT.allocateData(dataSize)) - return mode_static(); // allocation failed; //allocation failed - - spray = reinterpret_cast(SEGMENT.data); - // calculate the end of the spray data and assign it as the data pointer for the particles: - particles = reinterpret_cast(spray + numSprays); // cast the data array into a particle pointer - - uint32_t i = 0; - uint32_t j = 0; - //uint8_t spraycount = 1 + (SEGMENT.custom2 >> 5); // number of sprays to display, 1-8 - - if (SEGMENT.call == 0) // initialization - { - SEGMENT.aux0 = 0; // starting angle - SEGMENT.aux1 = 0xFF; // user check - for (i = 0; i < numParticles; i++) - { - PartSys->particles[i].ttl = 0; - } - for (i = 0; i < numSprays; i++) - { - PartSys->sources[i].source.hue = i*16; // even color distribution - PartSys->sources[i].source.sat = 255; // set saturation - PartSys->sources[i].source.x = (cols * PS_P_RADIUS) / 2; // center - PartSys->sources[i].source.y = (rows * PS_P_RADIUS) / 2; // center - PartSys->sources[i].source.vx = 0; - PartSys->sources[i].source.vy = 0; - PartSys->sources[i].maxLife = 400; - PartSys->sources[i].minLife = 200; - PartSys->sources[i].vx = 0; // emitting speed - PartSys->sources[i].vy = 0; // emitting speed - PartSys->sources[i].var = 0; // emitting variation - } - } - - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) - { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - - uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 - - i = 0; - - uint32_t threshold = 300 - SEGMENT.intensity; - - i = 0; - j = random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. - if (SEGMENT.check2) - SEGMENT.aux0 += SEGMENT.custom1; - else - SEGMENT.aux0 -= SEGMENT.custom1; - - uint32_t angleoffset = SEGMENT.aux0 >> 4; - - while (i < numParticles) - { - if (PartSys->particles[i].ttl == 0) // find a dead particle - { - uint8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed+10)) >> 9); // emit speed according to loudness of band - uint8_t emitangle = j * 16 + random16(SEGMENT.custom3 >> 1) + angleoffset; - - uint32_t emitparticles = 0; - if (fftResult[j] > threshold) - { - emitparticles = 1; // + (fftResult[bin]>>6); - } - else if (fftResult[j] > 0) // band has low volue - { - uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; - if (random16() % restvolume == 0) - { - emitparticles = 1; - } - } - if (emitparticles) - Emitter_Angle_emit(&spray[j], &PartSys->particles[i], emitangle, emitspeed); - j = (j + 1) % numSprays; - } - i++; - //todo: could add a break if all 16 sprays have been checked, would speed it up - } - - - - for (i = 0; i < numParticles; i++) - { - Particle_Move_update(&PartSys->particles[i], true); // move the particles, kill out of bounds particles - } - - - // render the particles - ParticleSys_render(particles, numParticles, false, false); - - - return FRAMETIME; -} - static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Color Change,Particle Speed,Spray Count,Nozzle Size,Random Color, Direction;;!;012;pal=56,sx=0,ix=222,c1=190,c2=200,c3=0,o1=0,o2=0"; -*/ - #endif //WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D @@ -9906,7 +9862,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); - // addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); #endif // WLED_DISABLE_PARTICLESYSTEM #endif // WLED_DISABLE_2D From 29f75de7202f4ada4879720c6f844ef18723c8dd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 27 May 2024 07:39:49 +0200 Subject: [PATCH 18/21] Bugfix and minor improvements - fixed bug in center GEQ - added '2D washing machine' mode for particle box - improved color-change rate in ghostrider - added AR to attractor (experimental, may remove again) --- wled00/FX.cpp | 52 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 857cc1cce1..c9010d3129 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7910,8 +7910,8 @@ uint16_t mode_particlevortex(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i = 0; - uint32_t j = 0; + uint32_t i, j; + if (SEGMENT.call == 0) // initialization { if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) @@ -8688,10 +8688,22 @@ uint16_t mode_particlebox(void) int32_t xgravity; int32_t ygravity; int32_t increment = (SEGMENT.speed >> 6) + 1; - if(SEGMENT.check2) // direction + + /*if(SEGMENT.check2) // direction SEGMENT.aux0 += increment; // update counter else SEGMENT.aux0 -= increment; + */ + + if(SEGMENT.check2) // washing machine + { + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGMENT.aux0 += speed; + if(speed == 0) SEGMENT.aux0 = 190; //down (= 270°) + } + else + SEGMENT.aux0 -= increment; + if(SEGMENT.check1) // random, use perlin noise { @@ -8722,7 +8734,7 @@ uint16_t mode_particlebox(void) return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Direction,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@Speed,Particles,Tilt Strength,Hardness,Friction,Random,Washing Machine,Sloshing;;!;2;pal=53,sx=120,ix=100,c1=100,c2=210,o1=1"; /* Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above @@ -9015,17 +9027,36 @@ uint16_t mode_particleattractor(void) else PartSys->angleEmit(PartSys->sources[0], SEGMENT.aux0 + 0x7FFF, 12); // emit at 180° as well // apply force + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if(usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + { + uint8_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); + uint8_t strength = volumeSmth; + if(SEGMENT.check3) strength = SEGMENT.speed; //AR disabled + for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles + { + PartSys->pointAttractor(i, attractor, strength, false); + } + } + #else for(i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } + #endif + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->particleMoveUpdate(PartSys->sources[0].source, &sourcesettings); // move the source PartSys->update(); // update and render return FRAMETIME; } +#ifdef USERMOD_AUDIOREACTIVE +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Disable AR;;!;2v;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +#else static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Particle Size,Collisions,Friction,Color by Age,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=0,c2=0,o1=0,o2=0,o3=0"; +#endif /* @@ -9326,7 +9357,7 @@ uint16_t mode_particleGEQ(void) PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames PartSys->particles[i].x = xposition + random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) - PartSys->particles[i].vx = random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 TODO: ok to use random16 here? + PartSys->particles[i].vx = random(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 PartSys->particles[i].vy = emitspeed; PartSys->particles[i].hue = (bin<<4) + random16(17) - 8; // color from palette according to bin emitparticles--; @@ -9380,7 +9411,7 @@ if (SEGLEN == 1) DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); return mode_static(); // something went wrong, no data! } - + PartSys->updateSystem(); // update system properties (dimensions and data pointers) numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); um_data_t *um_data; @@ -9427,7 +9458,7 @@ if (SEGLEN == 1) PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;012;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; +static const char _data_FX_MODE_PARTICLECCIRCULARGEQ[] PROGMEM = "PS Center GEQ@Speed,Intensity,Rotation Speed,Color Change,Nozzle Size,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8,o1=0,o2=0"; /* Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original by stepko adapted by Blaz Kristan (AKA blazoncek) @@ -9509,7 +9540,12 @@ uint16_t mode_particleghostrider(void) // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); - PartSys->sources[0].source.hue += SEGMENT.custom2 >> 3; + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + { + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; PartSys->update(); // update and render return FRAMETIME; From fc6c2dadc47f385db83e6a1f71a5ff7b2f138dc9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 27 May 2024 08:15:22 +0200 Subject: [PATCH 19/21] revert files to origin --- platformio.ini | 5 +++-- wled00/FX_fcn.cpp | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/platformio.ini b/platformio.ini index b419c11b99..504a1f3f71 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,8 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = platformio_override.ini +extra_configs = + platformio_override.ini [common] # ------------------------------------------------------------------------------ @@ -423,7 +424,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 ; 115200 230400 460800 +upload_speed = 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f71d14ddef..96df692cf1 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -149,11 +149,10 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } - DEBUG_PRINT(F("Allocating Data")); - // DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); + //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); deallocateData(); // if the old buffer was smaller release it first if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { // not enough memory From 16b03e7a591b847d15a8d29307a7096862c8e0c2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 30 Jun 2024 17:24:02 +0200 Subject: [PATCH 20/21] Huge update, added 1D version of particle system plus many new FX **2D FX candidates for replacement** - Fireworks 1D -> PS Fireworks (not a 1:1 replacement but similar) - Ghost Rider -> PS Ghost Rider - Blobs -> PS Blobs - Fire 2012 -> PS Fire (note: there is no 1D replacement yet) - Rain - 2D -> PS Ballpit, slow setting, no floor (very similar except dots are not fading) **1D FX candidates for replacement** - Bouncing Balls -> PS bouncing balls - Rolling Balls -> PS bouncing balls with 'Rolling' enabled - Dancing Shadows -> PS Dancing Shadows - Drip -> PS DripDrop - Fireworks 1D -> PS Fireworks 1D - Fireworks Starburst -> PS Starburst - Lighthouse -> PS Chase with 'Color by position', lowest density - Popcorn -> PS 1D Spray with disabled bounce - Rain: PS DriopDrop, 'Rain' check enabled, no splash, no blur, no gravity - Solid Glitter -> PS Sparkler (not worth disabling, only a few bytes) Some Notes: - Glitter & Sparkle-> could be replaced with PS Sparkler with overlay OR with an additional FX that draws the background first, maybe not possible on ESP8266 - Juggle could be replaced but will not save much (but would add more options) - Pride 2015 could be replaced by adding a 'pride' checkmark to PS Chase, woud save about 1kB - Colorwaves -> could be replaced with PS Chase with additional color setting for hue change, similar to pride. Would save another 1kB - Theater and Theater rainbow could be dropped -> PS Chase but it will not save much - Chase 2 & Chase 3 -> PS Chase (width can not be set as high as originals), share a function with Theater, so all have to be disabled (would save about 600 bytes) - 2D handling of some of the 1D replacements will be lost (like drip), I did not check all current FX in 2D - Freqmap, Freqwave and Freqmatrix (2.2kB total) could be replaced with a single FX using the PS, would save about 1kB - Blurring&Overlay only works if global buffer is enabled - All PS effects are frame-time based, there may be significant speed differences to the original effects if they use timers (and if FPS are low) Use these #defines to enable/disable PS or FX at compile time: WLED_DISABLE_PARTICLESYSTEM2D WLED_DISABLE_PARTICLESYSTEM1D DISABLE_2D_PS_REPLACEMENTS DISABLE_1D_PS_REPLACEMENTS --- CHANGELOG.md | 41 +- CONTRIBUTING.md | 4 +- package-lock.json | 4 +- package.json | 2 +- platformio.ini | 9 +- platformio_override.sample.ini | 29 +- requirements.txt | 4 +- usermods/LD2410_v2/readme.md | 36 + usermods/LD2410_v2/usermod_ld2410.h | 237 ++ .../usermod_PIR_sensor_switch.h | 4 + .../usermod_sn_photoresistor.h | 4 +- usermods/TetrisAI_v2/gridbw.h | 13 +- usermods/TetrisAI_v2/gridcolor.h | 8 + usermods/TetrisAI_v2/rating.h | 4 +- usermods/TetrisAI_v2/readme.md | 25 +- usermods/TetrisAI_v2/tetrisai.h | 111 +- usermods/TetrisAI_v2/tetrisaigame.h | 8 +- usermods/TetrisAI_v2/tetrisbag.h | 11 + usermods/TetrisAI_v2/usermod_v2_tetrisai.h | 76 +- usermods/smartnest/readme.md | 64 +- usermods/smartnest/usermod_smartnest.h | 38 +- .../usermod_v2_auto_save.h | 4 +- wled00/FX.cpp | 2773 ++++++++++++----- wled00/FX.h | 15 +- wled00/FX_fcn.cpp | 26 +- wled00/FXparticleSystem.cpp | 1256 ++++++-- wled00/FXparticleSystem.h | 247 +- wled00/bus_manager.cpp | 101 +- wled00/bus_manager.h | 19 +- wled00/bus_wrapper.h | 838 ++--- wled00/button.cpp | 58 +- wled00/cfg.cpp | 79 +- wled00/colors.cpp | 10 +- wled00/const.h | 66 +- wled00/data/index.css | 1 + wled00/data/index.htm | 3 +- wled00/data/index.js | 101 +- wled00/data/settings_leds.htm | 109 +- wled00/data/settings_wifi.htm | 20 +- wled00/improv.cpp | 2 +- wled00/json.cpp | 4 +- wled00/led.cpp | 2 +- wled00/network.cpp | 11 + wled00/pin_manager.cpp | 4 + wled00/set.cpp | 5 + wled00/usermods_list.cpp | 8 + wled00/util.cpp | 2 +- wled00/wled.cpp | 73 +- wled00/wled.h | 14 +- wled00/xml.cpp | 13 +- 50 files changed, 4470 insertions(+), 2126 deletions(-) create mode 100644 usermods/LD2410_v2/readme.md create mode 100644 usermods/LD2410_v2/usermod_ld2410.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 606d9240d9..b2801b8664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,47 @@ ## WLED changelog +#### Build 2406290 +- WLED 0.15.0-b4 release +- LED settings bus management update (WARNING only allow available outputs) +- Add ETH support for LILYGO-POE-Pro (#4030 by @rorosaurus) +- Update usermod_sn_photoresistor (#4017 by @xkvmoto) +- Several internal fixes and optimisations + - move LED_BUILTIN handling to BusManager class + - reduce max panels (web server limitation) + - edit WiFi TX power (ESP32) + - keep current ledmap ID in UI + - limit outputs in UI based on length + - wifi.ap addition to JSON Info (JSON API) + - relay pin init bugfix + - file editor button in UI + - ESP8266: update was restarting device on some occasions + - a bit of throttling in UI (for ESP8266) + +#### Build 2406120 +- Update NeoPixelBus to v2.8.0 +- Increased LED outputs one ESP32 using parallel I2S (up to 17) + - use single/mono I2S + 4x RMT for 5 outputs or less + - use parallel x8 I2S + 8x RMT for >5 outputs (limit of 300 LEDs per output) +- Fixed code of Smartnest and updated documentation (#4001 by @DevilPro1) +- ESP32-S3 WiFi fix (#4010 by @cstruck) +- TetrisAI usermod fix (#3897 by @muebau) +- ESP-NOW usermod hook +- Update wled.h regarding OTA Password (#3993 by @gsieben) +- Usermod BME68X Sensor Implementation (#3994 by @gsieben) +- Add a usermod for AHT10, AHT15 and AHT20 temperature/humidity sensors (#3977 by @LordMike) +- Update Battery usermod documentation (#3968 by @adamsthws) +- Add INA226 usermod for reading current and power over i2c (#3986 by @LordMike) +- Bugfixes: #3991 +- Several internal fixes and optimisations (WARNING: some effects may be broken that rely on overflow/narrow width) + - replace uint8_t and uint16_t with unsigned + - replace in8_t and int16_t with int + - reduces code by 1kB + #### Build 2405180 +- WLED 0.14.4 release +- Fix for #3978 - Official 0.15.0-b3 release -- Merge 0.14.3 fixes +- Merge 0.14.3 fixes into 0_15 - Added Pinwheel Expand 1D->2D effect mapping mode (#3961 by @Brandon502) - Add changeable i2c address to BME280 usermod (#3966 by @LordMike) - Effect: Firenoise - add palette selection diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f813999bb8..cd50b133d6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ You are all set if you have enabled `Editor: Detect Indentation` in VS Code. #### Blocks -Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable. +Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block brackets is acceptable. Good: ```cpp @@ -49,7 +49,7 @@ if (a == b) doStuff(a); ``` There should always be a space between a keyword and its condition and between the condition and brace. -Within the condition, no space should be between the paranthesis and variables. +Within the condition, no space should be between the parenthesis and variables. Spaces between variables and operators are up to the authors discretion. There should be no space between function names and their argument parenthesis. diff --git a/package-lock.json b/package-lock.json index b9dc5e0e3a..38f2099d62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.15.0-b3", + "version": "0.15.0-b4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wled", - "version": "0.15.0-b3", + "version": "0.15.0-b4", "license": "ISC", "dependencies": { "clean-css": "^5.3.3", diff --git a/package.json b/package.json index b19ecc48a4..e3c12629ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.15.0-b3", + "version": "0.15.0-b4", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 504a1f3f71..d75dc24658 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,8 +15,7 @@ default_envs = nodemcuv2, esp8266_2m, esp01_1m_full, nodemcuv2_160, esp8266_2m_1 src_dir = ./wled00 data_dir = ./wled00/data build_cache_dir = ~/.buildcache -extra_configs = - platformio_override.ini +extra_configs = platformio_override.ini [common] # ------------------------------------------------------------------------------ @@ -138,7 +137,8 @@ lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.6.0 IRremoteESP8266 @ 2.8.2 - makuna/NeoPixelBus @ 2.7.9 + makuna/NeoPixelBus @ 2.8.0 + #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta https://github.com/Aircoookie/ESPAsyncWebServer.git @ 2.2.1 # for I2C interface ;Wire @@ -424,7 +424,7 @@ build_flags = ${common.build_flags} ${esp32c3.build_flags} -D WLED_RELEASE_NAME= -DLOLIN_WIFI_FIX ; seems to work much better with this -DARDUINO_USB_CDC_ON_BOOT=1 ;; for virtual CDC USB ;-DARDUINO_USB_CDC_ON_BOOT=0 ;; for serial-to-USB chip -upload_speed = 460800 +upload_speed = 460800 ; 115200 230400 460800 build_unflags = ${common.build_unflags} lib_deps = ${esp32c3.lib_deps} @@ -480,6 +480,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_4M_qspi -DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") -DBOARD_HAS_PSRAM + -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 ${esp32.AR_build_flags} lib_deps = ${esp32s3.lib_deps} diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index 6becc5d7d1..be959e46a3 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -10,7 +10,7 @@ default_envs = WLED_tasmota_1M # define as many as you need #---------- # SAMPLE #---------- -[env:WLED_tasmota_1M] +[env:WLED_generic8266_1M] extends = env:esp01_1m_full # when you want to extend the existing environment (define only updated options) ; board = esp01_1m # uncomment when ou need different board ; platform = ${common.platform_wled_default} # uncomment and change when you want particular platform @@ -26,9 +26,9 @@ lib_deps = ${esp8266.lib_deps} ; adafruit/Adafruit BME280 Library@^2.2.2 ; Wire ; robtillaart/SHT85@~0.3.3 -; gmag11/QuickESPNow ;@ 0.6.2 +; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library -; https://github.com/kosme/arduinoFFT#develop @ 2.0.1 ;; used for USERMOD_AUDIOREACTIVE +; ${esp32.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; @@ -51,6 +51,11 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D WLED_DISABLE_ESPNOW ; -D WLED_DISABLE_BROWNOUT_DET ; +; enable optional built-in features +; -D WLED_ENABLE_PIXART +; -D WLED_ENABLE_USERMOD_PAGE # if created +; -D WLED_ENABLE_DMX +; ; PIN defines - uncomment and change, if needed: ; -D LEDPIN=2 ; or use this for multiple outputs @@ -64,6 +69,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; Limit max buses ; -D WLED_MAX_BUSSES=2 +; -D WLED_MAX_ANALOG_CHANNELS=3 # only 3 PWM HW pins available +; -D WLED_MAX_DIGITAL_CHANNELS=2 # only 2 HW accelerated pins available ; ; Configure default WiFi ; -D CLIENT_SSID='"MyNetwork"' @@ -128,12 +135,12 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; ; Use PIR sensor usermod and configure it to use GPIO4 and timer of 60s ; -D USERMOD_PIRSWITCH -; -D PIR_SENSOR_PIN=4 +; -D PIR_SENSOR_PIN=4 # use -1 to disable usermod ; -D PIR_SENSOR_OFF_SEC=60 +; -D PIR_SENSOR_MAX_SENSORS=2 # max allowable sensors (uses OR logic for triggering) ; ; Use Audioreactive usermod and configure I2S microphone ; -D USERMOD_AUDIOREACTIVE -; -D UM_AUDIOREACTIVE_USE_NEW_FFT ; -D AUDIOPIN=-1 ; -D DMTYPE=1 # 0-analog/disabled, 1-I2S generic, 2-ES7243, 3-SPH0645, 4-I2S+mclk, 5-I2S PDM ; -D I2S_SDPIN=36 @@ -155,18 +162,22 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D DEFAULT_LED_COUNT=30 ; or this for multiple outputs ; -D PIXEL_COUNTS=30,30 -; -; set milliampere limit when using ESP pin to power leds +; +; set the default LED type +; -D DEFAULT_LED_TYPE=22 # see const.h (TYPE_xxxx) +; +; set milliampere limit when using ESP power pin (or inadequate PSU) to power LEDs ; -D ABL_MILLIAMPS_DEFAULT=850 +; -D LED_MILLIAMPS_DEFAULT=55 ; ; enable IR by setting remote type -; -D IRTYPE=0 ;0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote +; -D IRTYPE=0 # 0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote ; ; set default color order of your led strip ; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB ; ; use PSRAM on classic ESP32 rev.1 (rev.3 or above has no issues) -; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue +; -DBOARD_HAS_PSRAM -mfix-esp32-psram-cache-issue # needed only for classic ESP32 rev.1 ; ; configure I2C and SPI interface (for various hardware) ; -D I2CSDAPIN=33 # initialise interface diff --git a/requirements.txt b/requirements.txt index 35c89a5e20..c4ce9445fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,7 +42,7 @@ pyelftools==0.29 # via platformio pyserial==3.5 # via platformio -requests==2.31.0 +requests==2.32.0 # via platformio semantic-version==2.10.0 # via platformio @@ -54,7 +54,7 @@ tabulate==0.9.0 # via platformio typing-extensions==4.11.0 # via starlette -urllib3==1.26.18 +urllib3==1.26.19 # via requests uvicorn==0.20.0 # via platformio diff --git a/usermods/LD2410_v2/readme.md b/usermods/LD2410_v2/readme.md new file mode 100644 index 0000000000..40463d4b0e --- /dev/null +++ b/usermods/LD2410_v2/readme.md @@ -0,0 +1,36 @@ +# BH1750 usermod + +> This usermod requires a second UART and was only tested on the ESP32 + + +This usermod will read from a LD2410 movement/presence sensor. + +The movement and presence state are displayed in both the Info section of the web UI, as well as published to the `/movement` and `/stationary` MQTT topics respectively. + +## Dependencies +- Libraries + - `ncmreynolds/ld2410@^0.1.3` + - This must be added under `lib_deps` in your `platformio.ini` (or `platformio_override.ini`). +- Data is published over MQTT - make sure you've enabled the MQTT sync interface. + +## Compilation + +To enable, compile with `USERMOD_LD2410` defined (e.g. in `platformio_override.ini`) +```ini +[env:usermod_USERMOD_LD2410_esp32dev] +extends = env:esp32dev +build_flags = + ${common.build_flags_esp32} + -D USERMOD_LD2410 +lib_deps = + ${esp32.lib_deps} + ncmreynolds/ld2410@^0.1.3 +``` + +### Configuration Options +The Usermod screen allows you to: +- enable/disable the usermod +- Configure the RX/TX pins + +## Change log +- 2024-06 Created by @wesleygas (https://github.com/wesleygas/) diff --git a/usermods/LD2410_v2/usermod_ld2410.h b/usermods/LD2410_v2/usermod_ld2410.h new file mode 100644 index 0000000000..4d96b32fff --- /dev/null +++ b/usermods/LD2410_v2/usermod_ld2410.h @@ -0,0 +1,237 @@ +#warning **** Included USERMOD_LD2410 **** + +#ifndef WLED_ENABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +#pragma once + +#include "wled.h" +#include + +class LD2410Usermod : public Usermod { + + private: + + bool enabled = true; + bool initDone = false; + bool sensorFound = false; + unsigned long lastTime = 0; + unsigned long last_mqtt_sent = 0; + + int8_t default_uart_rx = 19; + int8_t default_uart_tx = 18; + + + String mqttMovementTopic = F(""); + String mqttStationaryTopic = F(""); + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages + + + ld2410 radar; + bool stationary_detected = false; + bool last_stationary_state = false; + bool movement_detected = false; + bool last_movement_state = false; + + // These config variables have defaults set inside readFromConfig() + int8_t uart_rx_pin; + int8_t uart_tx_pin; + + // string that are used multiple time (this will save some flash memory) + static const char _name[]; + static const char _enabled[]; + + void publishMqtt(const char* topic, const char* state, bool retain); // example for publishing MQTT message + + void _mqttInitialize() + { + mqttMovementTopic = String(mqttDeviceTopic) + F("/ld2410/movement"); + mqttStationaryTopic = String(mqttDeviceTopic) + F("/ld2410/stationary"); + if (HomeAssistantDiscovery){ + _createMqttSensor(F("Movement"), mqttMovementTopic, F("motion"), F("")); + _createMqttSensor(F("Stationary"), mqttStationaryTopic, F("occupancy"), F("")); + } + } + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) + { + String t = String(F("homeassistant/binary_sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + + doc[F("name")] = String(serverDescription) + F(" Module"); + doc[F("state_topic")] = topic; + doc[F("unique_id")] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + if (deviceClass != "") + doc[F("device_class")] = deviceClass; + doc[F("expire_after")] = 1800; + doc[F("payload_off")] = "OFF"; + doc[F("payload_on")] = "ON"; + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = F("FOSS"); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); + } + + public: + + inline bool isEnabled() { return enabled; } + + void setup() { + Serial1.begin(256000, SERIAL_8N1, uart_rx_pin, uart_tx_pin); + Serial.print(F("\nLD2410 radar sensor initialising: ")); + if(radar.begin(Serial1)){ + Serial.println(F("OK")); + } else { + Serial.println(F("not connected")); + } + initDone = true; + } + + + void loop() { + // NOTE: on very long strips strip.isUpdating() may always return true so update accordingly + if (!enabled || strip.isUpdating()) return; + radar.read(); + unsigned long curr_time = millis(); + if(curr_time - lastTime > 1000) //Try to Report every 1000ms + { + lastTime = curr_time; + sensorFound = radar.isConnected(); + if(!sensorFound) return; + stationary_detected = radar.presenceDetected(); + if(stationary_detected != last_stationary_state){ + if (WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); + last_stationary_state = stationary_detected; + } + } + movement_detected = radar.movingTargetDetected(); + if(movement_detected != last_movement_state){ + if (WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); + last_movement_state = movement_detected; + } + } + // If there hasn't been any activity, send current state to confirm sensor is alive + if(curr_time - last_mqtt_sent > 1000*60*5 && WLED_MQTT_CONNECTED){ + publishMqtt("/ld2410/stationary", stationary_detected ? "ON":"OFF", false); + publishMqtt("/ld2410/movement", movement_detected ? "ON":"OFF", false); + } + } + } + + + void addToJsonInfo(JsonObject& root) + { + // if "u" object does not exist yet wee need to create it + JsonObject user = root[F("u")]; + if (user.isNull()) user = root.createNestedObject(F("u")); + + JsonArray ld2410_sta_json = user.createNestedArray(F("LD2410 Stationary")); + JsonArray ld2410_mov_json = user.createNestedArray(F("LD2410 Movement")); + if (!enabled){ + ld2410_sta_json.add(F("disabled")); + ld2410_mov_json.add(F("disabled")); + } else if(!sensorFound){ + ld2410_sta_json.add(F("LD2410")); + ld2410_sta_json.add(" Not Found"); + } else { + ld2410_sta_json.add("Sta "); + ld2410_sta_json.add(stationary_detected ? "ON":"OFF"); + ld2410_mov_json.add("Mov "); + ld2410_mov_json.add(movement_detected ? "ON":"OFF"); + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + //save these vars persistently whenever settings are saved + top["uart_rx_pin"] = default_uart_rx; + top["uart_tx_pin"] = default_uart_tx; + } + + + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + if (!configComplete) + { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINT(F("LD2410")); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + configComplete &= getJsonValue(top["uart_rx_pin"], uart_rx_pin, default_uart_rx); + configComplete &= getJsonValue(top["uart_tx_pin"], uart_tx_pin, default_uart_tx); + + return configComplete; + } + + +#ifndef WLED_DISABLE_MQTT + /** + * onMqttConnect() is called when MQTT connection is established + */ + void onMqttConnect(bool sessionPresent) { + // do any MQTT related initialisation here + if(!radar.isConnected()) return; + publishMqtt("/ld2410/status", "I am alive!", false); + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + } +#endif + + uint16_t getId() + { + return USERMOD_ID_LD2410; + } +}; + + +// add more strings here to reduce flash memory usage +const char LD2410Usermod::_name[] PROGMEM = "LD2410Usermod"; +const char LD2410Usermod::_enabled[] PROGMEM = "enabled"; + + +// implementation of non-inline member methods + +void LD2410Usermod::publishMqtt(const char* topic, const char* state, bool retain) +{ +#ifndef WLED_DISABLE_MQTT + //Check if MQTT Connected, otherwise it will crash + if (WLED_MQTT_CONNECTED) { + last_mqtt_sent = millis(); + char subuf[64]; + strcpy(subuf, mqttDeviceTopic); + strcat(subuf, topic); + mqtt->publish(subuf, 0, retain, state); + } +#endif +} diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index d66b1b333e..7a67dd7497 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -58,7 +58,11 @@ class PIRsensorSwitch : public Usermod bool sensorPinState[PIR_SENSOR_MAX_SENSORS] = {LOW}; // current PIR sensor pin state // configurable parameters +#if PIR_SENSOR_PIN < 0 + bool enabled = false; // PIR sensor disabled +#else bool enabled = true; // PIR sensor enabled +#endif int8_t PIRsensorPin[PIR_SENSOR_MAX_SENSORS] = {PIR_SENSOR_PIN}; // PIR sensor pin uint32_t m_switchOffDelay = PIR_SENSOR_OFF_SEC*1000; // delay before switch off after the sensor state goes LOW (10min) uint8_t m_onPreset = 0; // on preset diff --git a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h index c09b5d9075..45cdb66ad7 100644 --- a/usermods/SN_Photoresistor/usermod_sn_photoresistor.h +++ b/usermods/SN_Photoresistor/usermod_sn_photoresistor.h @@ -3,7 +3,9 @@ #include "wled.h" //Pin defaults for QuinLed Dig-Uno (A0) +#ifndef PHOTORESISTOR_PIN #define PHOTORESISTOR_PIN A0 +#endif // the frequency to check photoresistor, 10 seconds #ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL @@ -207,4 +209,4 @@ const char Usermod_SN_Photoresistor::_readInterval[] PROGMEM = "read-interval-s" const char Usermod_SN_Photoresistor::_referenceVoltage[] PROGMEM = "supplied-voltage"; const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value"; const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; -const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; \ No newline at end of file +const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h index af3f5bcf02..deea027d79 100644 --- a/usermods/TetrisAI_v2/gridbw.h +++ b/usermods/TetrisAI_v2/gridbw.h @@ -34,7 +34,7 @@ class GridBW { if (width > 32) { - throw std::invalid_argument("maximal width is 32"); + this->width = 32; } } @@ -112,6 +112,17 @@ class GridBW { return pixels[y] == (uint32_t)((1 << width) - 1); } + + void reset() + { + if (width > 32) + { + width = 32; + } + + pixels.clear(); + pixels.resize(height); + } }; #endif /* __GRIDBW_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridcolor.h b/usermods/TetrisAI_v2/gridcolor.h index 5c5ce7e422..815c2a5603 100644 --- a/usermods/TetrisAI_v2/gridcolor.h +++ b/usermods/TetrisAI_v2/gridcolor.h @@ -127,6 +127,14 @@ class GridColor } } } + + void reset() + { + gridBW.reset(); + pixels.clear(); + pixels.resize(width* height); + clear(); + } }; #endif /* __GRIDCOLOR_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/rating.h b/usermods/TetrisAI_v2/rating.h index 4504ff468b..88320818e1 100644 --- a/usermods/TetrisAI_v2/rating.h +++ b/usermods/TetrisAI_v2/rating.h @@ -32,7 +32,7 @@ class Rating uint8_t fullLines; uint16_t bumpiness; uint16_t aggregatedHeight; - double score; + float score; uint8_t width; std::vector lineHights; @@ -57,7 +57,7 @@ class Rating this->fullLines = 0; this->bumpiness = 0; this->aggregatedHeight = 0; - this->score = -DBL_MAX; + this->score = -FLT_MAX; } }; diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md index b21bc5fde1..b56f801a87 100644 --- a/usermods/TetrisAI_v2/readme.md +++ b/usermods/TetrisAI_v2/readme.md @@ -1,16 +1,22 @@ # Tetris AI effect usermod -This usermod brings you a effect brings a self playing Tetris game. The mod needs version 0.14 or above as it is based on matrix support. The effect was tested on an ESP32 with a WS2812B 16x16 matrix. +This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix. Version 1.0 ## Installation -Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/Aircoookie/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). + +If needed simply add to `platformio_override.ini` (or `platformio_override.ini`): + +```ini +board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv +``` ## Usage -It is best to set the background color to black, the border color to light grey and the game over color (foreground) to dark grey. +It is best to set the background color to black 🖤, the border color to light grey 🤍, the game over color (foreground) to dark grey 🩶, and color palette to 'Rainbow' 🌈. ### Sliders and boxes @@ -19,15 +25,18 @@ It is best to set the background color to black, the border color to light grey * speed: speed the game plays * look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2) * intelligence: how good the AI will play -* Rotate color: make the colors shift (rotate) every few cicles -* Mistakes free: how many good moves between mistakes (if activated) +* Rotate color: make the colors shift (rotate) every few moves +* Mistakes free: how many good moves between mistakes (if enabled) #### Checkboxes -* show next: if true a space of 5 pixels from the right is used to show the next pieces. The whole segment is used for the grid otherwise. +* show next: if true, a space of 5 pixels from the right will be used to show the next pieces. Otherwise the whole segment is used for the grid. * show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces -* mistakes: if true the worst instead of the best move is choosen every few moves (read above) +* mistakes: if true, the worst decision will be made every few moves instead of the best (see above). ## Best results - If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party. \ No newline at end of file + If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party 😉. + +## Limits +The game grid is limited to a maximum width of 32 and a maximum height of 255 due to the internal structure of the code. The canvas of the effect will be centred in the segment if the segment exceeds the maximum width or height. \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h index ef7868a47c..ba4fe60e43 100644 --- a/usermods/TetrisAI_v2/tetrisai.h +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -22,10 +22,10 @@ class TetrisAI { private: public: - double aHeight; - double fullLines; - double holes; - double bumpiness; + float aHeight; + float fullLines; + float holes; + float bumpiness; bool findWorstMove = false; uint8_t countOnes(uint32_t vector) @@ -107,10 +107,10 @@ class TetrisAI rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness)); } - TetrisAI(): TetrisAI(-0.510066, 0.760666, -0.35663, -0.184483) + TetrisAI(): TetrisAI(-0.510066f, 0.760666f, -0.35663f, -0.184483f) {} - TetrisAI(double aHeight, double fullLines, double holes, double bumpiness): + TetrisAI(float aHeight, float fullLines, float holes, float bumpiness): aHeight(aHeight), fullLines(fullLines), holes(holes), @@ -178,9 +178,9 @@ class TetrisAI if(findWorstMove) { //init rating for worst - if(bestRating->score == -DBL_MAX) + if(bestRating->score == -FLT_MAX) { - bestRating->score = DBL_MAX; + bestRating->score = FLT_MAX; } // update if we found a worse one @@ -202,101 +202,6 @@ class TetrisAI } } } - - bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) - { - //vector with pieces - //for every piece - //for every - switch (expression) - { - case INIT: - break; - - default: - break; - } - } - - bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) - { - //INIT - grid.cleanupFullLines(); - Rating curRating(grid.width); - Rating deeperRating(grid.width); - Piece piece = *start; - - // for every rotation of the piece - piece.rotation = 0; - - //HANDLE - while (piece.rotation < piece.pieceData->rotCount) - { - // put piece to top left corner - piece.x = 0; - piece.y = 0; - - //test for every column - piece.x = 0; - while (piece.x <= grid.width - piece.getRotation().width) - { - - //todo optimise by the use of the previous grids height - piece.landingY = 0; - //will set landingY to final position - grid.findLandingPosition(&piece); - - // draw piece - grid.placePiece(&piece, piece.x, piece.landingY); - - if(start == end - 1) - { - //at the deepest level - updateRating(grid, &curRating); - } - else - { - //go deeper to take another piece into account - findBestMove(grid, start + 1, end, &deeperRating); - curRating = deeperRating; - } - - // eraese piece - grid.erasePiece(&piece, piece.x, piece.landingY); - - if(findWorstMove) - { - //init rating for worst - if(bestRating->score == -DBL_MAX) - { - bestRating->score = DBL_MAX; - } - - // update if we found a worse one - if (bestRating->score > curRating.score) - { - *bestRating = curRating; - (*start) = piece; - } - } - else - { - // update if we found a better one - if (bestRating->score < curRating.score) - { - *bestRating = curRating; - (*start) = piece; - } - } - piece.x++; - } - piece.rotation++; - } - - //EXIT - - return true; - } }; #endif /* __AI_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisaigame.h b/usermods/TetrisAI_v2/tetrisaigame.h index de3c86e7e3..e4766d18b6 100644 --- a/usermods/TetrisAI_v2/tetrisaigame.h +++ b/usermods/TetrisAI_v2/tetrisaigame.h @@ -54,6 +54,7 @@ class TetrisAIGame uint8_t width; uint8_t height; uint8_t nLookAhead; + uint8_t nPieces; TetrisBag bag; GridColor grid; TetrisAI ai; @@ -65,6 +66,7 @@ class TetrisAIGame width(width), height(height), nLookAhead(nLookAhead), + nPieces(nPieces), bag(nPieces, 1, nLookAhead), grid(width, height + 4), ai(), @@ -142,8 +144,10 @@ class TetrisAIGame void reset() { - grid.clear(); - bag.init(); + grid.width = width; + grid.height = height + 4; + grid.reset(); + bag.reset(); } }; diff --git a/usermods/TetrisAI_v2/tetrisbag.h b/usermods/TetrisAI_v2/tetrisbag.h index 3ecadbc0be..592dac6c7f 100644 --- a/usermods/TetrisAI_v2/tetrisbag.h +++ b/usermods/TetrisAI_v2/tetrisbag.h @@ -25,6 +25,7 @@ class TetrisBag public: uint8_t nPieces; uint8_t nBagLength; + uint8_t queueLength; uint8_t bagIdx; std::vector bag; std::vector piecesQueue; @@ -32,6 +33,7 @@ class TetrisBag TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength): nPieces(nPieces), nBagLength(nBagLength), + queueLength(queueLength), bag(nPieces * nBagLength), piecesQueue(queueLength) { @@ -95,6 +97,15 @@ class TetrisBag std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces); } + + void reset() + { + bag.clear(); + bag.resize(nPieces * nBagLength); + piecesQueue.clear(); + piecesQueue.resize(queueLength); + init(); + } }; #endif /* __TETRISBAG_H__ */ diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h index 1c077d0485..0f7039dac9 100644 --- a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h +++ b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h @@ -18,6 +18,12 @@ typedef struct TetrisAI_data uint8_t colorOffset; uint8_t colorInc; uint8_t mistaceCountdown; + uint16_t segcols; + uint16_t segrows; + uint16_t segOffsetX; + uint16_t segOffsetY; + uint16_t effectWidth; + uint16_t effectHeight; } tetrisai_data; void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) @@ -49,7 +55,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); } - SEGMENT.setPixelColorXY(index_x, index_y - 4, color); + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + index_x, tetrisai_data->segOffsetY + index_y - 4, color); } } tetrisai_data->colorOffset += tetrisai_data->colorInc; @@ -61,14 +67,14 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) if (tetrisai_data->showBorder) { //draw a line 6 pixels from right with the border color - for (auto index_y = 0; index_y < SEGMENT.virtualHeight(); index_y++) + for (auto index_y = 0; index_y < tetrisai_data->effectHeight; index_y++) { - SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() - 6, index_y, SEGCOLOR(2)); + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + tetrisai_data->effectWidth - 6, tetrisai_data->segOffsetY + index_y, SEGCOLOR(2)); } } //NEXT PIECE - int piecesOffsetX = SEGMENT.virtualWidth() - 4; + int piecesOffsetX = tetrisai_data->effectWidth - 4; int piecesOffsetY = 1; for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++) { @@ -83,7 +89,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) if (piece.getPixel(pieceX, pieceY)) { uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset); - SEGMENT.setPixelColorXY(piecesOffsetX + pieceX, piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); + SEGMENT.setPixelColorXY(tetrisai_data->segOffsetX + piecesOffsetX + pieceX, tetrisai_data->segOffsetY + piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); } } } @@ -116,62 +122,86 @@ uint16_t mode_2DTetrisAI() //range 0 - 16 tetrisai_data->colorInc = SEGMENT.custom2 >> 4; - if (!tetrisai_data->tetris || (tetrisai_data->tetris.nLookAhead != nLookAhead + if (tetrisai_data->tetris.nLookAhead != nLookAhead + || tetrisai_data->segcols != cols + || tetrisai_data->segrows != rows || tetrisai_data->showNext != SEGMENT.check1 || tetrisai_data->showBorder != SEGMENT.check2 - ) - ) + ) { + tetrisai_data->segcols = cols; + tetrisai_data->segrows = rows; tetrisai_data->showNext = SEGMENT.check1; tetrisai_data->showBorder = SEGMENT.check2; - //not more than 32 as this is the limit of this implementation - uint8_t gridWidth = cols < 32 ? cols : 32; - uint8_t gridHeight = rows; + //not more than 32 columns and 255 rows as this is the limit of this implementation + uint8_t gridWidth = cols > 32 ? 32 : cols; + uint8_t gridHeight = rows > 255 ? 255 : rows; + + tetrisai_data->effectWidth = 0; + tetrisai_data->effectHeight = 0; // do we need space for the 'next' section? if (tetrisai_data->showNext) { - // make space for the piece and one pixel of space - gridWidth = gridWidth - 5; + //does it get to tight? + if (gridWidth + 5 > cols) + { + // yes, so make the grid smaller + // make space for the piece and one pixel of space + gridWidth = (gridWidth - ((gridWidth + 5) - cols)); + } + tetrisai_data->effectWidth += 5; // do we need space for a border? if (tetrisai_data->showBorder) { - gridWidth = gridWidth - 1; + if (gridWidth + 5 + 1 > cols) + { + gridWidth -= 1; + } + tetrisai_data->effectWidth += 1; } } + tetrisai_data->effectWidth += gridWidth; + tetrisai_data->effectHeight += gridHeight; + + tetrisai_data->segOffsetX = cols > tetrisai_data->effectWidth ? ((cols - tetrisai_data->effectWidth) / 2) : 0; + tetrisai_data->segOffsetY = rows > tetrisai_data->effectHeight ? ((rows - tetrisai_data->effectHeight) / 2) : 0; + tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); + tetrisai_data->tetris.state = TetrisAIGame::States::INIT; SEGMENT.fill(SEGCOLOR(1)); } if (tetrisai_data->intelligence != SEGMENT.custom1) { tetrisai_data->intelligence = SEGMENT.custom1; - double dui = 0.2 - (0.2 * (tetrisai_data->intelligence / 255.0)); + float dui = 0.2f - (0.2f * (tetrisai_data->intelligence / 255.0f)); - tetrisai_data->tetris.ai.aHeight = -0.510066 + dui; - tetrisai_data->tetris.ai.fullLines = 0.760666 - dui; - tetrisai_data->tetris.ai.holes = -0.35663 + dui; - tetrisai_data->tetris.ai.bumpiness = -0.184483 + dui; + tetrisai_data->tetris.ai.aHeight = -0.510066f + dui; + tetrisai_data->tetris.ai.fullLines = 0.760666f - dui; + tetrisai_data->tetris.ai.holes = -0.35663f + dui; + tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui; } if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) { - if (millis() - tetrisai_data->lastTime > msDelayMove) + + if (strip.now - tetrisai_data->lastTime > msDelayMove) { drawGrid(&tetrisai_data->tetris, tetrisai_data); - tetrisai_data->lastTime = millis(); + tetrisai_data->lastTime = strip.now; tetrisai_data->tetris.poll(); } } else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) { - if (millis() - tetrisai_data->lastTime > msDelayGameOver) + if (strip.now - tetrisai_data->lastTime > msDelayGameOver) { drawGrid(&tetrisai_data->tetris, tetrisai_data); - tetrisai_data->lastTime = millis(); + tetrisai_data->lastTime = strip.now; tetrisai_data->tetris.poll(); } } diff --git a/usermods/smartnest/readme.md b/usermods/smartnest/readme.md index 5c3ef8072e..62bfcdada4 100644 --- a/usermods/smartnest/readme.md +++ b/usermods/smartnest/readme.md @@ -1,61 +1,41 @@ # Smartnest -Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants. +Enables integration with `smartnest.cz` service which provides MQTT integration with voice assistants, for example Google Home, Alexa, Siri, Home Assistant and more! + In order to setup Smartnest follow the [documentation](https://www.docu.smartnest.cz/). + - You can create up to 5 different devices + - To add the project to Google Home you can find the information [here](https://www.docu.smartnest.cz/google-home-integration) + - To add the project to Alexa you can find the information [here](https://www.docu.smartnest.cz/alexa-integration) ## MQTT API The API is described in the Smartnest [Github repo](https://github.com/aososam/Smartnest/blob/master/Devices/lightRgb/lightRgb.ino). - ## Usermod installation -1. Register the usermod by adding `#include "../usermods/smartnest/usermod_smartnest.h"` at the top and `usermods.add(new Smartnest());` at the bottom of `usermods_list.cpp`. -or -2. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini - - -Example **usermods_list.cpp**: - -```cpp -#include "wled.h" -/* - * Register your v2 usermods here! - * (for v1 usermods using just usermod.cpp, you can ignore this file) - */ - -/* - * Add/uncomment your usermod filename here (and once more below) - * || || || - * \/ \/ \/ - */ -//#include "usermod_v2_example.h" -//#include "usermod_temperature.h" -#include "../usermods/usermod_smartnest.h" - -void registerUsermods() -{ - /* - * Add your usermod class name here - * || || || - * \/ \/ \/ - */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - usermods.add(new Smartnest()); - -} -``` +1. Use `#define USERMOD_SMARTNEST` in wled.h or `-D USERMOD_SMARTNEST` in your platformio.ini (recommended). ## Configuration Usermod has no configuration, but it relies on the MQTT configuration.\ Under Config > Sync Interfaces > MQTT: -* Enable MQTT check box -* Set the `Broker` field to: `smartnest.cz` -* The `Username` and `Password` fields are the login information from the `smartnest.cz` website. + +* Enable `MQTT` check box. +* Set the `Broker` field to: `smartnest.cz` or `3.122.209.170`(both work). +* Set the `Port` field to: `1883` +* The `Username` and `Password` fields are the login information from the `smartnest.cz` website (It is located above in the 3 points). * `Client ID` field is obtained from the device configuration panel in `smartnest.cz`. +* `Device Topic` is obtained by entering the ClientID/report , remember to replace ClientId with your real information (Because they can ban your device). +* `Group Topic` keep the same Group Topic. + +Wait `1 minute` after turning it on, as it usually takes a while. ## Change log + 2022-09 -* First implementation. + * First implementation. + +2024-05 + * Solved code. + * Updated documentation. + * Second implementation. diff --git a/usermods/smartnest/usermod_smartnest.h b/usermods/smartnest/usermod_smartnest.h index 8d2b04ff96..92d524c88a 100644 --- a/usermods/smartnest/usermod_smartnest.h +++ b/usermods/smartnest/usermod_smartnest.h @@ -9,6 +9,10 @@ class Smartnest : public Usermod { private: + bool initialized = false; + unsigned long lastMqttReport = 0; + unsigned long mqttReportInterval = 60000; // Report every minute + void sendToBroker(const char *const topic, const char *const message) { if (!WLED_MQTT_CONNECTED) @@ -61,7 +65,7 @@ class Smartnest : public Usermod int position = 0; // We need to copy the string in order to keep it read only as strtok_r function requires mutable string - color_ = (char *)malloc(strlen(color)); + color_ = (char *)malloc(strlen(color) + 1); if (NULL == color_) { return -1; } @@ -150,7 +154,7 @@ class Smartnest : public Usermod delay(100); sendToBroker("report/firmware", versionString); // Reports the firmware version delay(100); - sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the ip + sendToBroker("report/ip", (char *)WiFi.localIP().toString().c_str()); // Reports the IP delay(100); sendToBroker("report/network", (char *)WiFi.SSID().c_str()); // Reports the network name delay(100); @@ -168,4 +172,34 @@ class Smartnest : public Usermod { return USERMOD_ID_SMARTNEST; } + + /** + * setup() is called once at startup to initialize the usermod. + */ + void setup() { + DEBUG_PRINTF("Smartnest usermod setup initializing..."); + + // Publish initial status + sendToBroker("report/status", "Smartnest usermod initialized"); + } + + /** + * loop() is called continuously to keep the usermod running. + */ + void loop() { + // Periodically report status to MQTT broker + unsigned long currentMillis = millis(); + if (currentMillis - lastMqttReport >= mqttReportInterval) { + lastMqttReport = currentMillis; + + // Report current brightness + char brightnessMsg[11]; + sprintf(brightnessMsg, "%u", bri); + sendToBroker("report/brightness", brightnessMsg); + + // Report current signal strength + String signal(WiFi.RSSI(), 10); + sendToBroker("report/signal", signal.c_str()); + } + } }; diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index 2a63dd4d88..52ff3cc1db 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -122,9 +122,9 @@ class AutoSaveUsermod : public Usermod { * Da loop. */ void loop() { - if (!autoSaveAfterSec || !enabled || strip.isUpdating() || currentPreset>0) return; // setting 0 as autosave seconds disables autosave - + static unsigned long lastRun = 0; unsigned long now = millis(); + if (!autoSaveAfterSec || !enabled || currentPreset>0 || (strip.isUpdating() && now - lastRun < 240)) return; // setting 0 as autosave seconds disables autosave uint8_t currentMode = strip.getMainSegment().mode; uint8_t currentPalette = strip.getMainSegment().palette; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3944f5d01a..a6ffe5d968 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -29,7 +29,10 @@ #include "wled.h" #include "FX.h" #include "fcn_declare.h" + +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include "FXparticleSystem.h" +#endif #define IBN 5100 @@ -163,9 +166,10 @@ static const char _data_FX_MODE_STROBE_RAINBOW[] PROGMEM = "Strobe Rainbow@!;,!; * if (bool rev == true) then LEDs are turned off in reverse order */ uint16_t color_wipe(bool rev, bool useRandomColors) { + if (SEGLEN == 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; + unsigned prog = (perc * 65535) / cycleTime; bool back = (prog > 32767); if (back) { prog -= 32767; @@ -189,16 +193,16 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } } - uint16_t ledIndex = (prog * SEGLEN) >> 15; - uint16_t rem = 0; + unsigned ledIndex = (prog * SEGLEN) >> 15; + unsigned rem = 0; rem = (prog * SEGLEN) * 2; //mod 0xFFFF rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; uint32_t col1 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux1) : SEGCOLOR(1); - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { - uint16_t index = (rev && back)? SEGLEN -1 -i : i; + unsigned index = (rev && back)? SEGLEN -1 -i : i; uint32_t col0 = useRandomColors? SEGMENT.color_wheel(SEGENV.aux0) : SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0); if (i < ledIndex) @@ -259,7 +263,7 @@ uint16_t mode_random_color(void) { uint32_t cycleTime = 200 + (255 - SEGMENT.speed)*50; uint32_t it = strip.now / cycleTime; uint32_t rem = strip.now % cycleTime; - uint16_t fadedur = (cycleTime * SEGMENT.intensity) >> 8; + unsigned fadedur = (cycleTime * SEGMENT.intensity) >> 8; uint32_t fade = 255; if (fadedur) { @@ -337,9 +341,9 @@ static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;; * Does the "standby-breathing" of well known i-Devices. */ uint16_t mode_breath(void) { - uint16_t var = 0; - uint16_t counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 + unsigned var = 0; + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); + counter = ((counter >> 2) + (counter >> 4)) & 0xFFFFU; //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); var = sin16(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 @@ -359,7 +363,7 @@ static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; * Fades the LEDs between two colors */ uint16_t mode_fade(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 3) +10)); + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); uint8_t lum = triwave16(counter) >> 8; for (int i = 0; i < SEGLEN; i++) { @@ -374,13 +378,13 @@ static const char _data_FX_MODE_FADE[] PROGMEM = "Fade@!;!,!;!;01"; /* * Scan mode parent function */ -uint16_t scan(bool dual) -{ +uint16_t scan(bool dual) { + if (SEGLEN == 1) return mode_static(); uint32_t cycleTime = 750 + (255 - SEGMENT.speed)*150; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; - uint16_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); - uint16_t ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; + int prog = (perc * 65535) / cycleTime; + int size = 1 + ((SEGMENT.intensity * SEGLEN) >> 9); + int ledIndex = (prog * ((SEGLEN *2) - size *2)) >> 16; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); @@ -389,7 +393,7 @@ uint16_t scan(bool dual) if (dual) { for (int j = led_offset; j < led_offset + size; j++) { - uint16_t i2 = SEGLEN -1 -j; + unsigned i2 = SEGLEN -1 -j; SEGMENT.setPixelColor(i2, SEGMENT.color_from_palette(i2, true, PALETTE_SOLID_WRAP, (SEGCOLOR(2))? 2:0)); } } @@ -424,7 +428,7 @@ static const char _data_FX_MODE_DUAL_SCAN[] PROGMEM = "Scan Dual@!,# of dots,,,, * Cycles all LEDs at once through a rainbow. */ uint16_t mode_rainbow(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; if (SEGMENT.intensity < 128){ @@ -442,7 +446,7 @@ static const char _data_FX_MODE_RAINBOW[] PROGMEM = "Colorloop@!,Saturation;;!;0 * Cycles a rainbow over the entire string of LEDs. */ uint16_t mode_rainbow_cycle(void) { - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; for (int i = 0; i < SEGLEN; i++) { @@ -459,8 +463,8 @@ static const char _data_FX_MODE_RAINBOW_CYCLE[] PROGMEM = "Rainbow@!,Size;;!"; /* * Alternating pixels running function. */ -uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { - uint8_t width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window +static uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { + int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window uint32_t cycleTime = 50 + (255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; bool usePalette = color1 == SEGCOLOR(0); @@ -471,7 +475,7 @@ uint16_t running(uint32_t color1, uint32_t color2, bool theatre = false) { if (theatre) { if ((i % width) == SEGENV.aux0) col = color1; } else { - int8_t pos = (i % (width<<1)); + int pos = (i % (width<<1)); if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; } SEGMENT.setPixelColor(i,col); @@ -508,12 +512,12 @@ static const char _data_FX_MODE_THEATER_CHASE_RAINBOW[] PROGMEM = "Theater Rainb /* * Running lights effect with smooth sine transition base. */ -uint16_t running_base(bool saw, bool dual=false) { - uint8_t x_scale = SEGMENT.intensity >> 2; +static uint16_t running_base(bool saw, bool dual=false) { + unsigned x_scale = SEGMENT.intensity >> 2; uint32_t counter = (strip.now * SEGMENT.speed) >> 9; for (int i = 0; i < SEGLEN; i++) { - uint16_t a = i*x_scale - counter; + unsigned a = i*x_scale - counter; if (saw) { a &= 0xFF; if (a < 16) @@ -527,7 +531,7 @@ uint16_t running_base(bool saw, bool dual=false) { uint8_t s = dual ? sin_gap(a) : sin8(a); uint32_t ca = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s); if (dual) { - uint16_t b = (SEGLEN-1-i)*x_scale - counter; + unsigned b = (SEGLEN-1-i)*x_scale - counter; uint8_t t = sin_gap(b); uint32_t cb = color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), t); ca = color_blend(ca, cb, 127); @@ -578,7 +582,7 @@ uint16_t mode_twinkle(void) { uint32_t it = strip.now / cycleTime; if (it != SEGENV.step) { - uint16_t maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on + unsigned maxOn = map(SEGMENT.intensity, 0, 255, 1, SEGLEN); // make sure at least one LED is on if (SEGENV.aux0 >= maxOn) { SEGENV.aux0 = 0; @@ -588,13 +592,13 @@ uint16_t mode_twinkle(void) { SEGENV.step = it; } - uint16_t PRNG16 = SEGENV.aux1; + unsigned PRNG16 = SEGENV.aux1; for (unsigned i = 0; i < SEGENV.aux0; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next 'random' number uint32_t p = (uint32_t)SEGLEN * (uint32_t)PRNG16; - uint16_t j = p >> 16; + unsigned j = p >> 16; SEGMENT.setPixelColor(j, SEGMENT.color_from_palette(j, true, PALETTE_SOLID_WRAP, 0)); } @@ -607,7 +611,7 @@ static const char _data_FX_MODE_TWINKLE[] PROGMEM = "Twinkle@!,!;!,!;!;;m12=0"; * Dissolve function */ uint16_t dissolve(uint32_t color) { - uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED + unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed if (SEGENV.call == 0) { @@ -747,7 +751,7 @@ uint16_t mode_multi_strobe(void) { } SEGENV.aux0 = 50 + 20*(uint16_t)(255-SEGMENT.speed); - uint16_t count = 2 * ((SEGMENT.intensity / 10) + 1); + unsigned count = 2 * ((SEGMENT.intensity / 10) + 1); if(SEGENV.aux1 < count) { if((SEGENV.aux1 & 1) == 0) { SEGMENT.fill(SEGCOLOR(0)); @@ -785,7 +789,7 @@ uint16_t mode_android(void) { if (SEGENV.aux1 < 2) SEGENV.aux0 = 0; } - uint16_t a = SEGENV.step; + unsigned a = SEGENV.step & 0xFFFFU; if (SEGENV.aux0 == 0) { @@ -801,15 +805,15 @@ uint16_t mode_android(void) { if (a + SEGENV.aux1 < SEGLEN) { - for (int i = a; i < a+SEGENV.aux1; i++) { + for (unsigned i = a; i < a+SEGENV.aux1; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } else { - for (int i = a; i < SEGLEN; i++) { + for (unsigned i = a; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } - for (int i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { + for (unsigned i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } } @@ -825,7 +829,7 @@ static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12= * color1 = background color * color2 and color3 = colors of two adjacent leds */ -uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { +static uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palette) { uint16_t counter = strip.now * ((SEGMENT.speed >> 2) + 1); uint16_t a = (counter * SEGLEN) >> 16; @@ -841,7 +845,7 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett SEGENV.step = a; // Use intensity setting to vary chase up to 1/2 string length - uint8_t size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); + unsigned size = 1 + ((SEGMENT.intensity * SEGLEN) >> 10); uint16_t b = a + size; //"trail" of chase, filled with color1 if (b > SEGLEN) b -= SEGLEN; @@ -851,7 +855,7 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett //background if (do_palette) { - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } } else SEGMENT.fill(color1); @@ -860,31 +864,31 @@ uint16_t chase(uint32_t color1, uint32_t color2, uint32_t color3, bool do_palett if (chase_random) { color1 = SEGMENT.color_wheel(SEGENV.aux1); - for (int i = a; i < SEGLEN; i++) + for (unsigned i = a; i < SEGLEN; i++) SEGMENT.setPixelColor(i, color1); } //fill between points a and b with color2 if (a < b) { - for (int i = a; i < b; i++) + for (unsigned i = a; i < b; i++) SEGMENT.setPixelColor(i, color2); } else { - for (int i = a; i < SEGLEN; i++) //fill until end + for (unsigned i = a; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color2); - for (int i = 0; i < b; i++) //fill from start until b + for (unsigned i = 0; i < b; i++) //fill from start until b SEGMENT.setPixelColor(i, color2); } //fill between points b and c with color2 if (b < c) { - for (int i = b; i < c; i++) + for (unsigned i = b; i < c; i++) SEGMENT.setPixelColor(i, color3); } else { - for (int i = b; i < SEGLEN; i++) //fill until end + for (unsigned i = b; i < SEGLEN; i++) //fill until end SEGMENT.setPixelColor(i, color3); - for (int i = 0; i < c; i++) //fill from start until c + for (unsigned i = 0; i < c; i++) //fill from start until c SEGMENT.setPixelColor(i, color3); } @@ -914,9 +918,9 @@ static const char _data_FX_MODE_CHASE_RANDOM[] PROGMEM = "Chase Random@!,Width;! * Primary, secondary running on rainbow. */ uint16_t mode_chase_rainbow(void) { - uint8_t color_sep = 256 / SEGLEN; + unsigned color_sep = 256 / SEGLEN; if (color_sep == 0) color_sep = 1; // correction for segments longer than 256 LEDs - uint8_t color_index = SEGENV.call & 0xFF; + unsigned color_index = SEGENV.call & 0xFF; uint32_t color = SEGMENT.color_wheel(((SEGENV.step * color_sep) + color_index) & 0xFF); return chase(color, SEGCOLOR(0), SEGCOLOR(1), false); @@ -942,14 +946,14 @@ static const char _data_FX_MODE_CHASE_RAINBOW_WHITE[] PROGMEM = "Rainbow Runner@ * Red - Amber - Green - Blue lights running */ uint16_t mode_colorful(void) { - uint8_t numColors = 4; //3, 4, or 5 + unsigned numColors = 4; //3, 4, or 5 uint32_t cols[9]{0x00FF0000,0x00EEBB00,0x0000EE00,0x000077CC}; if (SEGMENT.intensity > 160 || SEGMENT.palette) { //palette or color if (!SEGMENT.palette) { numColors = 3; for (size_t i = 0; i < 3; i++) cols[i] = SEGCOLOR(i); } else { - uint16_t fac = 80; + unsigned fac = 80; if (SEGMENT.palette == 52) {numColors = 5; fac = 61;} //C9 2 has 5 colors for (size_t i = 0; i < numColors; i++) { cols[i] = SEGMENT.color_from_palette(i*fac, false, true, 255); @@ -973,9 +977,9 @@ uint16_t mode_colorful(void) { SEGENV.step = it; } - for (int i = 0; i < SEGLEN; i+= numColors) + for (unsigned i = 0; i < SEGLEN; i+= numColors) { - for (int j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); + for (unsigned j = 0; j < numColors; j++) SEGMENT.setPixelColor(i + j, cols[SEGENV.aux0 + j]); } return FRAMETIME; @@ -1021,17 +1025,17 @@ static const char _data_FX_MODE_TRAFFIC_LIGHT[] PROGMEM = "Traffic Light@!,US st #define FLASH_COUNT 4 uint16_t mode_chase_flash(void) { if (SEGLEN == 1) return mode_static(); - uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); + unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } - uint16_t delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); + unsigned delay = 10 + ((30 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { if(flash_step % 2 == 0) { - uint16_t n = SEGENV.step; - uint16_t m = (SEGENV.step + 1) % SEGLEN; + unsigned n = SEGENV.step; + unsigned m = (SEGENV.step + 1) % SEGLEN; SEGMENT.setPixelColor( n, SEGCOLOR(1)); SEGMENT.setPixelColor( m, SEGCOLOR(1)); delay = 20; @@ -1051,16 +1055,16 @@ static const char _data_FX_MODE_CHASE_FLASH[] PROGMEM = "Chase Flash@!;Bg,Fx;!"; */ uint16_t mode_chase_flash_random(void) { if (SEGLEN == 1) return mode_static(); - uint8_t flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); + unsigned flash_step = SEGENV.call % ((FLASH_COUNT * 2) + 1); for (int i = 0; i < SEGENV.aux1; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_wheel(SEGENV.aux0)); } - uint16_t delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); + unsigned delay = 1 + ((10 * (uint16_t)(255 - SEGMENT.speed)) / SEGLEN); if(flash_step < (FLASH_COUNT * 2)) { - uint16_t n = SEGENV.aux1; - uint16_t m = (SEGENV.aux1 + 1) % SEGLEN; + unsigned n = SEGENV.aux1; + unsigned m = (SEGENV.aux1 + 1) % SEGLEN; if(flash_step % 2 == 0) { SEGMENT.setPixelColor( n, SEGCOLOR(0)); SEGMENT.setPixelColor( m, SEGCOLOR(0)); @@ -1099,14 +1103,14 @@ uint16_t mode_running_random(void) { uint32_t it = strip.now / cycleTime; if (SEGENV.call == 0) SEGENV.aux0 = random16(); // random seed for PRNG on start - uint8_t zoneSize = ((255-SEGMENT.intensity) >> 4) +1; + unsigned zoneSize = ((255-SEGMENT.intensity) >> 4) +1; uint16_t PRNG16 = SEGENV.aux0; - uint8_t z = it % zoneSize; + unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (int i=SEGLEN-1; i > 0; i--) { + for (unsigned i=SEGLEN-1; i > 0; i--) { if (nzone || z >= zoneSize) { - uint8_t lastrand = PRNG16 >> 8; + unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; while (abs(diff) < 42) { // make sure the difference between adjacent colors is big enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 13849; // next zone, next 'random' number @@ -1131,7 +1135,7 @@ static const char _data_FX_MODE_RUNNING_RANDOM[] PROGMEM = "Stream@!,Zone size;; /* * K.I.T.T. */ -uint16_t mode_larson_scanner(void){ +uint16_t mode_larson_scanner(void) { if (SEGLEN == 1) return mode_static(); const unsigned speed = FRAMETIME * map(SEGMENT.speed, 0, 255, 96, 2); // map into useful range @@ -1185,25 +1189,25 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS /* * Firing comets from one end. "Lighthouse" */ uint16_t mode_comet(void) { if (SEGLEN == 1) return mode_static(); - uint16_t counter = strip.now * ((SEGMENT.speed >>2) +1); - uint16_t index = (counter * SEGLEN) >> 16; + unsigned counter = (strip.now * ((SEGMENT.speed >>2) +1)) & 0xFFFF; + unsigned index = (counter * SEGLEN) >> 16; if (SEGENV.call == 0) SEGENV.aux0 = index; SEGMENT.fade_out(SEGMENT.intensity); SEGMENT.setPixelColor( index, SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0)); if (index > SEGENV.aux0) { - for (int i = SEGENV.aux0; i < index ; i++) { + for (unsigned i = SEGENV.aux0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } else if (index < SEGENV.aux0 && index < 10) { - for (int i = 0; i < index ; i++) { + for (unsigned i = 0; i < index ; i++) { SEGMENT.setPixelColor( i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0)); } } @@ -1212,7 +1216,7 @@ uint16_t mode_comet(void) { return FRAMETIME; } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - +#endif // DISABLE_1D_PS_REPLACEMENTS /* * Fireworks function. @@ -1254,12 +1258,12 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN == 1) return mode_static(); - const uint16_t width = SEGMENT.virtualWidth(); - const uint16_t height = SEGMENT.virtualHeight(); + const unsigned width = SEGMENT.virtualWidth(); + const unsigned height = SEGMENT.virtualHeight(); SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; @@ -1288,7 +1292,7 @@ uint16_t mode_rain() { return mode_fireworks(); } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - +#endif //DISABLE_1D_PS_REPLACEMENTS /* * Fire flicker function @@ -1368,15 +1372,15 @@ static const char _data_FX_MODE_LOADING[] PROGMEM = "Loading@!,Fade;!,!;!;;ix=16 //American Police Light with all LEDs Red and Blue uint16_t police_base(uint32_t color1, uint32_t color2) { if (SEGLEN == 1) return mode_static(); - uint16_t delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster + unsigned delay = 1 + (FRAMETIME<<3) / SEGLEN; // longer segments should change faster uint32_t it = strip.now / map(SEGMENT.speed, 0, 255, delay<<4, delay); - uint16_t offset = it % SEGLEN; + unsigned offset = it % SEGLEN; - uint16_t width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip + unsigned width = ((SEGLEN*(SEGMENT.intensity+1))>>9); //max width is half the strip if (!width) width = 1; - for (int i = 0; i < width; i++) { - uint16_t indexR = (offset + i) % SEGLEN; - uint16_t indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; + for (unsigned i = 0; i < width; i++) { + unsigned indexR = (offset + i) % SEGLEN; + unsigned indexB = (offset + i + (SEGLEN>>1)) % SEGLEN; SEGMENT.setPixelColor(indexR, color1); SEGMENT.setPixelColor(indexB, color2); } @@ -1394,8 +1398,7 @@ uint16_t police_base(uint32_t color1, uint32_t color2) { //Police Lights with custom colors -uint16_t mode_two_dots() -{ +uint16_t mode_two_dots() { if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(2)); uint32_t color2 = (SEGCOLOR(1) == SEGCOLOR(2)) ? SEGCOLOR(0) : SEGCOLOR(1); return police_base(SEGCOLOR(0), color2); @@ -1426,27 +1429,27 @@ uint16_t mode_fairy() { //amount of flasher pixels depending on intensity (0: none, 255: every LED) if (SEGMENT.intensity == 0) return FRAMETIME; - uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 - uint16_t numFlashers = (SEGLEN / flasherDistance) +1; + unsigned flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 + unsigned numFlashers = (SEGLEN / flasherDistance) +1; - uint16_t dataSize = sizeof(flasher) * numFlashers; + unsigned dataSize = sizeof(flasher) * numFlashers; if (!SEGENV.allocateData(dataSize)) return FRAMETIME; //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = strip.now & 0xFFFF; + unsigned now16 = strip.now & 0xFFFF; //Up to 11 flashers in one brightness zone, afterwards a new zone for every 6 flashers - uint16_t zones = numFlashers/FLASHERS_PER_ZONE; + unsigned zones = numFlashers/FLASHERS_PER_ZONE; if (!zones) zones = 1; - uint8_t flashersInZone = numFlashers/zones; + unsigned flashersInZone = numFlashers/zones; uint8_t flasherBri[FLASHERS_PER_ZONE*2 -1]; - for (int z = 0; z < zones; z++) { - uint16_t flasherBriSum = 0; - uint16_t firstFlasher = z*flashersInZone; + for (unsigned z = 0; z < zones; z++) { + unsigned flasherBriSum = 0; + unsigned firstFlasher = z*flashersInZone; if (z == zones-1) flashersInZone = numFlashers-(flashersInZone*(zones-1)); - for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; + for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + unsigned stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 10) { flashers[f].stateOn = !flashers[f].stateOn; @@ -1471,15 +1474,15 @@ uint16_t mode_fairy() { flasherBriSum += flasherBri[f - firstFlasher]; } //dim factor, to create "shimmer" as other pixels get less voltage if a lot of flashers are on - uint8_t avgFlasherBri = flasherBriSum / flashersInZone; - uint8_t globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers + unsigned avgFlasherBri = flasherBriSum / flashersInZone; + unsigned globalPeakBri = 255 - ((avgFlasherBri * MAX_SHIMMER) >> 8); //183-255, suitable for 1/5th of LEDs flashers - for (int f = firstFlasher; f < firstFlasher + flashersInZone; f++) { - uint8_t bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; + for (unsigned f = firstFlasher; f < firstFlasher + flashersInZone; f++) { + unsigned bri = (flasherBri[f - firstFlasher] * globalPeakBri) / 255; PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number - uint16_t flasherPos = f*flasherDistance; + unsigned flasherPos = f*flasherDistance; SEGMENT.setPixelColor(flasherPos, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0), bri)); - for (int i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { + for (unsigned i = flasherPos+1; i < flasherPos+flasherDistance && i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(PRNG16 >> 8, false, false, 0, globalPeakBri)); } @@ -1495,17 +1498,17 @@ static const char _data_FX_MODE_FAIRY[] PROGMEM = "Fairy@!,# of flashers;!,!;!"; * Warning: Uses 4 bytes of segment data per pixel */ uint16_t mode_fairytwinkle() { - uint16_t dataSize = sizeof(flasher) * SEGLEN; + unsigned dataSize = sizeof(flasher) * SEGLEN; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Flasher* flashers = reinterpret_cast(SEGENV.data); - uint16_t now16 = strip.now & 0xFFFF; + unsigned now16 = strip.now & 0xFFFF; uint16_t PRNG16 = 5100 + strip.getCurrSegmentId(); - uint16_t riseFallTime = 400 + (255-SEGMENT.speed)*3; - uint16_t maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); + unsigned riseFallTime = 400 + (255-SEGMENT.speed)*3; + unsigned maxDur = riseFallTime/100 + ((255 - SEGMENT.intensity) >> 2) + 13 + ((255 - SEGMENT.intensity) >> 1); for (int f = 0; f < SEGLEN; f++) { - uint16_t stateTime = now16 - flashers[f].stateStart; + unsigned stateTime = now16 - flashers[f].stateStart; //random on/off time reached, switch state if (stateTime > flashers[f].stateDur * 100) { flashers[f].stateOn = !flashers[f].stateOn; @@ -1525,10 +1528,10 @@ uint16_t mode_fairytwinkle() { } if (flashers[f].stateOn && flashers[f].stateDur > maxDur) flashers[f].stateDur = maxDur; //react more quickly on intensity change if (stateTime > riseFallTime) stateTime = riseFallTime; //for flasher brightness calculation, fades in first 255 ms of state - uint8_t fadeprog = 255 - ((stateTime * 255) / riseFallTime); - uint8_t flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); - uint16_t lastR = PRNG16; - uint16_t diff = 0; + unsigned fadeprog = 255 - ((stateTime * 255) / riseFallTime); + unsigned flasherBri = (flashers[f].stateOn) ? 255-gamma8(fadeprog) : gamma8(fadeprog); + unsigned lastR = PRNG16; + unsigned diff = 0; while (diff < 0x4000) { //make sure colors of two adjacent LEDs differ enough PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; //next 'random' number diff = (PRNG16 > lastR) ? PRNG16 - lastR : lastR - PRNG16; @@ -1546,8 +1549,8 @@ static const char _data_FX_MODE_FAIRYTWINKLE[] PROGMEM = "Fairytwinkle@!,!;!,!;! uint16_t tricolor_chase(uint32_t color1, uint32_t color2) { uint32_t cycleTime = 50 + ((255 - SEGMENT.speed)<<1); uint32_t it = strip.now / cycleTime; // iterator - uint8_t width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour - uint8_t index = it % (width*3); + unsigned width = (1 + (SEGMENT.intensity>>4)); // value of 1-16 for each colour + unsigned index = it % (width*3); for (int i = 0; i < SEGLEN; i++, index++) { if (index > (width*3)-1) index = 0; @@ -1575,8 +1578,8 @@ static const char _data_FX_MODE_TRICOLOR_CHASE[] PROGMEM = "Chase 3@!,Size;1,2,3 * ICU mode */ uint16_t mode_icu(void) { - uint16_t dest = SEGENV.step & 0xFFFF; - uint8_t space = (SEGMENT.intensity >> 3) +2; + unsigned dest = SEGENV.step & 0xFFFF; + unsigned space = (SEGMENT.intensity >> 3) +2; if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); @@ -1618,9 +1621,9 @@ static const char _data_FX_MODE_ICU[] PROGMEM = "ICU@!,!,,,,,Overlay;!,!;!"; uint16_t mode_tricolor_wipe(void) { uint32_t cycleTime = 1000 + (255 - SEGMENT.speed)*200; uint32_t perc = strip.now % cycleTime; - uint16_t prog = (perc * 65535) / cycleTime; - uint16_t ledIndex = (prog * SEGLEN * 3) >> 16; - uint16_t ledOffset = ledIndex; + unsigned prog = (perc * 65535) / cycleTime; + unsigned ledIndex = (prog * SEGLEN * 3) >> 16; + unsigned ledOffset = ledIndex; for (int i = 0; i < SEGLEN; i++) { @@ -1628,20 +1631,20 @@ uint16_t mode_tricolor_wipe(void) { } if(ledIndex < SEGLEN) { //wipe from 0 to 1 - for (int i = 0; i < SEGLEN; i++) + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (i > ledOffset)? SEGCOLOR(0) : SEGCOLOR(1)); } } else if (ledIndex < SEGLEN*2) { //wipe from 1 to 2 ledOffset = ledIndex - SEGLEN; - for (int i = ledOffset +1; i < SEGLEN; i++) + for (unsigned i = ledOffset +1; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } } else //wipe from 2 to 0 { ledOffset = ledIndex - SEGLEN*2; - for (int i = 0; i <= ledOffset; i++) + for (unsigned i = 0; i <= ledOffset; i++) { SEGMENT.setPixelColor(i, SEGCOLOR(0)); } @@ -1658,11 +1661,11 @@ static const char _data_FX_MODE_TRICOLOR_WIPE[] PROGMEM = "Tri Wipe@!;1,2,3;!"; * Modified by Aircoookie */ uint16_t mode_tricolor_fade(void) { - uint16_t counter = strip.now * ((SEGMENT.speed >> 3) +1); - uint32_t prog = (counter * 768) >> 16; + unsigned counter = strip.now * ((SEGMENT.speed >> 3) +1); + uint16_t prog = (counter * 768) >> 16; uint32_t color1 = 0, color2 = 0; - byte stage = 0; + unsigned stage = 0; if(prog < 256) { color1 = SEGCOLOR(0); @@ -1679,7 +1682,7 @@ uint16_t mode_tricolor_fade(void) { } byte stp = prog; // % 256 - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color; if (stage == 2) { color = color_blend(SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 2), color2, stp); @@ -1700,19 +1703,20 @@ static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h */ +#define MAX_COMETS 8 uint16_t mode_multi_comet(void) { uint32_t cycleTime = 10 + (uint32_t)(255 - SEGMENT.speed); uint32_t it = strip.now / cycleTime; if (SEGENV.step == it) return FRAMETIME; - if (!SEGENV.allocateData(sizeof(uint16_t) * 8)) return mode_static(); //allocation failed + if (!SEGENV.allocateData(sizeof(uint16_t) * MAX_COMETS)) return mode_static(); //allocation failed - SEGMENT.fade_out(SEGMENT.intensity); + SEGMENT.fade_out(SEGMENT.intensity/2 + 128); uint16_t* comets = reinterpret_cast(SEGENV.data); - for (int i=0; i < 8; i++) { + for (unsigned i=0; i < MAX_COMETS; i++) { if(comets[i] < SEGLEN) { - uint16_t index = comets[i]; + unsigned index = comets[i]; if (SEGCOLOR(2) != 0) { SEGMENT.setPixelColor(index, i % 2 ? SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(2)); @@ -1731,8 +1735,8 @@ uint16_t mode_multi_comet(void) { SEGENV.step = it; return FRAMETIME; } -static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet"; - +static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; +#undef MAX_COMETS /* * Running random pixels ("Stream 2") @@ -1743,19 +1747,19 @@ uint16_t mode_random_chase(void) { SEGENV.step = RGBW32(random8(), random8(), random8(), 0); SEGENV.aux0 = random16(); } - uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function + unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function uint32_t cycleTime = 25 + (3 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; uint32_t color = SEGENV.step; random16_set_seed(SEGENV.aux0); - for (int i = SEGLEN -1; i > 0; i--) { + for (unsigned i = SEGLEN -1; i > 0; i--) { uint8_t r = random8(6) != 0 ? (color >> 16 & 0xFF) : random8(); uint8_t g = random8(6) != 0 ? (color >> 8 & 0xFF) : random8(); uint8_t b = random8(6) != 0 ? (color & 0xFF) : random8(); color = RGBW32(r, g, b, 0); - SEGMENT.setPixelColor(i, r, g, b); - if (i == SEGLEN -1 && SEGENV.aux1 != (it & 0xFFFF)) { //new first color in next frame + SEGMENT.setPixelColor(i, color); + if (i == SEGLEN -1U && SEGENV.aux1 != (it & 0xFFFFU)) { //new first color in next frame SEGENV.step = color; SEGENV.aux0 = random16_get_seed(); } @@ -1771,18 +1775,18 @@ static const char _data_FX_MODE_RANDOM_CHASE[] PROGMEM = "Stream 2@!;;"; //7 bytes typedef struct Oscillator { - int16_t pos; - int8_t size; - int8_t dir; - int8_t speed; + uint16_t pos; + uint8_t size; + int8_t dir; + uint8_t speed; } oscillator; /* / Oscillating bars of color, updated with standard framerate */ uint16_t mode_oscillate(void) { - uint8_t numOscillators = 3; - uint16_t dataSize = sizeof(oscillator) * numOscillators; + unsigned numOscillators = 3; + unsigned dataSize = sizeof(oscillator) * numOscillators; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -1790,15 +1794,15 @@ uint16_t mode_oscillate(void) { if (SEGENV.call == 0) { - oscillators[0] = {(int16_t)(SEGLEN/4), (int8_t)(SEGLEN/8), 1, 1}; - oscillators[1] = {(int16_t)(SEGLEN/4*3), (int8_t)(SEGLEN/8), 1, 2}; - oscillators[2] = {(int16_t)(SEGLEN/4*2), (int8_t)(SEGLEN/8), -1, 1}; + oscillators[0] = {(uint16_t)(SEGLEN/4), (uint8_t)(SEGLEN/8), 1, 1}; + oscillators[1] = {(uint16_t)(SEGLEN/4*3), (uint8_t)(SEGLEN/8), 1, 2}; + oscillators[2] = {(uint16_t)(SEGLEN/4*2), (uint8_t)(SEGLEN/8), -1, 1}; } uint32_t cycleTime = 20 + (2 * (uint32_t)(255 - SEGMENT.speed)); uint32_t it = strip.now / cycleTime; - for (int i = 0; i < numOscillators; i++) { + for (unsigned i = 0; i < numOscillators; i++) { // if the counter has increased, move the oscillator by the random step if (it != SEGENV.step) oscillators[i].pos += oscillators[i].dir * oscillators[i].speed; oscillators[i].size = SEGLEN/(3+SEGMENT.intensity/8); @@ -1815,10 +1819,10 @@ uint16_t mode_oscillate(void) { } } - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { uint32_t color = BLACK; - for (int j = 0; j < numOscillators; j++) { - if(i >= oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { + for (unsigned j = 0; j < numOscillators; j++) { + if(i >= (unsigned)oscillators[j].pos - oscillators[j].size && i <= oscillators[j].pos + oscillators[j].size) { color = (color == BLACK) ? SEGCOLOR(j) : color_blend(color, SEGCOLOR(j), 128); } } @@ -1834,8 +1838,8 @@ static const char _data_FX_MODE_OSCILLATE[] PROGMEM = "Oscillate"; //TODO uint16_t mode_lightning(void) { if (SEGLEN == 1) return mode_static(); - uint16_t ledstart = random16(SEGLEN); // Determine starting location of flash - uint16_t ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) + unsigned ledstart = random16(SEGLEN); // Determine starting location of flash + unsigned ledlen = 1 + random16(SEGLEN -ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1) uint8_t bri = 255/random8(1, 3); if (SEGENV.aux1 == 0) //init, leader flash @@ -1850,7 +1854,7 @@ uint16_t mode_lightning(void) { if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); if (SEGENV.aux1 > 3 && !(SEGENV.aux1 & 0x01)) { //flash on even number >2 - for (int i = ledstart; i < ledstart + ledlen; i++) + for (unsigned i = ledstart; i < ledstart + ledlen; i++) { SEGMENT.setPixelColor(i,SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0, bri)); } @@ -1879,30 +1883,30 @@ static const char _data_FX_MODE_LIGHTNING[] PROGMEM = "Lightning@!,!,,,,,Overlay // Animated, ever-changing rainbows. // by Mark Kriegsman: https://gist.github.com/kriegsman/964de772d64c502760e5 uint16_t mode_pride_2015(void) { - uint16_t duration = 10 + SEGMENT.speed; - uint16_t sPseudotime = SEGENV.step; - uint16_t sHue16 = SEGENV.aux0; + unsigned duration = 10 + SEGMENT.speed; + unsigned sPseudotime = SEGENV.step; + unsigned sHue16 = SEGENV.aux0; uint8_t sat8 = beatsin88( 87, 220, 250); uint8_t brightdepth = beatsin88( 341, 96, 224); - uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); - uint8_t msmultiplier = beatsin88(147, 23, 60); + unsigned brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256)); + unsigned msmultiplier = beatsin88(147, 23, 60); - uint16_t hue16 = sHue16;//gHue * 256; - uint16_t hueinc16 = beatsin88(113, 1, 3000); + unsigned hue16 = sHue16;//gHue * 256; + unsigned hueinc16 = beatsin88(113, 1, 3000); sPseudotime += duration * msmultiplier; sHue16 += duration * beatsin88( 400, 5,9); - uint16_t brightnesstheta16 = sPseudotime; + unsigned brightnesstheta16 = sPseudotime; - for (int i = 0 ; i < SEGLEN; i++) { + for (unsigned i = 0 ; i < SEGLEN; i++) { hue16 += hueinc16; uint8_t hue8 = hue16 >> 8; brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16( brightnesstheta16 ) + 32768; + unsigned b16 = sin16( brightnesstheta16 ) + 32768; - uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; + unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); @@ -1925,7 +1929,7 @@ uint16_t mode_juggle(void) { CRGB fastled_col; byte dothue = 0; for (int i = 0; i < 8; i++) { - uint16_t index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); + int index = 0 + beatsin88((16 + SEGMENT.speed)*(i + 7), 0, SEGLEN -1); fastled_col = CRGB(SEGMENT.getPixelColor(index)); fastled_col |= (SEGMENT.palette==0)?CHSV(dothue, 220, 255):ColorFromPalette(SEGPALETTE, dothue, 255); SEGMENT.setPixelColor(index, fastled_col); @@ -1941,7 +1945,7 @@ uint16_t mode_palette() { #ifdef ESP8266 using mathType = int32_t; using wideMathType = int64_t; - using angleType = uint16_t; + using angleType = unsigned; constexpr mathType sInt16Scale = 0x7FFF; constexpr mathType maxAngle = 0x8000; constexpr mathType staticRotationScale = 256; @@ -2034,7 +2038,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;c1=128,c2=128,c3=128,o1=1,o2=1,o3=0"; - +#ifndef DISABLE_2D_PS_REPLACEMENTS // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2065,7 +2069,7 @@ static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation // in step 3 above) (Effect Intensity = Sparking). uint16_t mode_fire_2012() { if (SEGLEN == 1) return mode_static(); - const uint16_t strips = SEGMENT.nrOfVStrips(); + const unsigned strips = SEGMENT.nrOfVStrips(); if (!SEGENV.allocateData(strips * SEGLEN)) return mode_static(); //allocation failed byte* heat = SEGENV.data; @@ -2105,7 +2109,7 @@ uint16_t mode_fire_2012() { } }; - for (int stripNr=0; stripNr> 8; - uint16_t h16_128 = hue16 >> 7; + unsigned h16_128 = hue16 >> 7; if ( h16_128 & 0x100) { hue8 = 255 - (h16_128 >> 1); } else { @@ -2148,9 +2152,9 @@ uint16_t mode_colorwaves() { } brightnesstheta16 += brightnessthetainc16; - uint16_t b16 = sin16(brightnesstheta16) + 32768; + unsigned b16 = sin16(brightnesstheta16) + 32768; - uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; + unsigned bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; bri8 += (255 - brightdepth); @@ -2179,11 +2183,8 @@ static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; uint16_t mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = random16(12345); - //CRGB fastled_col; for (int i = 0; i < SEGLEN; i++) { - uint8_t index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); - //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8(SEGMENT.speed, 1, 6); //10,1,4 @@ -2194,21 +2195,18 @@ static const char _data_FX_MODE_FILLNOISE8[] PROGMEM = "Fill Noise@!;!;!"; uint16_t mode_noise16_1() { - uint16_t scale = 320; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 320; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed/16); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm - uint16_t shift_y = SEGENV.step/42; // the y position becomes slowly incremented - uint16_t real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm - uint16_t real_y = (i + shift_y) * scale; // the y position becomes slowly incremented + unsigned shift_x = beatsin8(11); // the x position of the noise field swings @ 17 bpm + unsigned shift_y = SEGENV.step/42; // the y position becomes slowly incremented + unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm + unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map LED color based on noise data + unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map LED color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, 255, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } @@ -2218,18 +2216,15 @@ static const char _data_FX_MODE_NOISE16_1[] PROGMEM = "Noise 1@!;!;!"; uint16_t mode_noise16_2() { - uint16_t scale = 1000; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 1000; // the "zoom factor" for the noise SEGENV.step += (1 + (SEGMENT.speed >> 1)); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = SEGENV.step >> 6; // x as a function of time + unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - uint8_t noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + unsigned noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map led color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } @@ -2239,21 +2234,18 @@ static const char _data_FX_MODE_NOISE16_2[] PROGMEM = "Noise 2@!;!;!"; uint16_t mode_noise16_3() { - uint16_t scale = 800; // the "zoom factor" for the noise - //CRGB fastled_col; + unsigned scale = 800; // the "zoom factor" for the noise SEGENV.step += (1 + SEGMENT.speed); for (int i = 0; i < SEGLEN; i++) { - uint16_t shift_x = 4223; // no movement along x and y - uint16_t shift_y = 1234; + unsigned shift_x = 4223; // no movement along x and y + unsigned shift_y = 1234; uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; - uint8_t noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down - uint8_t index = sin8(noise * 3); // map led color based on noise data + unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned index = sin8(noise * 3); // map led color based on noise data - //fastled_col = ColorFromPalette(SEGPALETTE, index, noise, LINEARBLEND); // With that value, look up the 8 bit colour palette value and assign it to the current LED. - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); } @@ -2264,12 +2256,9 @@ static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!"; //https://github.com/aykevl/ledstrip-spark/blob/master/ledstrip.ino uint16_t mode_noise16_4() { - //CRGB fastled_col; uint32_t stp = (strip.now * SEGMENT.speed) >> 7; for (int i = 0; i < SEGLEN; i++) { - int16_t index = inoise16(uint32_t(i) << 12, stp); - //fastled_col = ColorFromPalette(SEGPALETTE, index); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); + int index = inoise16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; @@ -2279,7 +2268,7 @@ static const char _data_FX_MODE_NOISE16_4[] PROGMEM = "Noise 4@!;!;!"; //based on https://gist.github.com/kriegsman/5408ecd397744ba0393e uint16_t mode_colortwinkle() { - uint16_t dataSize = (SEGLEN+7) >> 3; //1 bit per LED + unsigned dataSize = (SEGLEN+7) >> 3; //1 bit per LED if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGB fastled_col, prev; @@ -2288,8 +2277,8 @@ uint16_t mode_colortwinkle() { for (int i = 0; i < SEGLEN; i++) { fastled_col = SEGMENT.getPixelColor(i); prev = fastled_col; - uint16_t index = i >> 3; - uint8_t bitNum = i & 0x07; + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; bool fadeUp = bitRead(SEGENV.data[index], bitNum); if (fadeUp) { @@ -2318,8 +2307,8 @@ uint16_t mode_colortwinkle() { int i = random16(SEGLEN); if (SEGMENT.getPixelColor(i) == 0) { fastled_col = ColorFromPalette(SEGPALETTE, random8(), 64, NOBLEND); - uint16_t index = i >> 3; - uint8_t bitNum = i & 0x07; + unsigned index = i >> 3; + unsigned bitNum = i & 0x07; bitWrite(SEGENV.data[index], bitNum, true); SEGMENT.setPixelColor(i, fastled_col); break; //only spawn 1 new pixel per frame per 50 LEDs @@ -2334,18 +2323,15 @@ static const char _data_FX_MODE_COLORTWINKLE[] PROGMEM = "Colortwinkles@Fade spe //Calm effect, like a lake at night uint16_t mode_lake() { - uint8_t sp = SEGMENT.speed/10; + unsigned sp = SEGMENT.speed/10; int wave1 = beatsin8(sp +2, -64,64); int wave2 = beatsin8(sp +1, -64,64); - uint8_t wave3 = beatsin8(sp +2, 0,80); - //CRGB fastled_col; + int wave3 = beatsin8(sp +2, 0,80); for (int i = 0; i < SEGLEN; i++) { int index = cos8((i*15)+ wave1)/2 + cubicwave8((i*23)+ wave2)/2; uint8_t lum = (index > wave3) ? index - wave3 : 0; - //fastled_col = ColorFromPalette(SEGPALETTE, map(index,0,255,0,240), lum, LINEARBLEND); - //SEGMENT.setPixelColor(i, fastled_col.red, fastled_col.green, fastled_col.blue); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, false, 0, lum)); } @@ -2363,8 +2349,8 @@ uint16_t mode_meteor() { byte* trail = SEGENV.data; - const unsigned meteorSize= 1 + SEGLEN / 20; // 5% - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); + const unsigned meteorSize = 1 + SEGLEN / 20; // 5% + unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; @@ -2380,7 +2366,7 @@ uint16_t mode_meteor() { // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; + unsigned index = in + j; if (index >= SEGLEN) { index -= SEGLEN; } @@ -2403,12 +2389,12 @@ uint16_t mode_meteor_smooth() { byte* trail = SEGENV.data; - const unsigned meteorSize= 1+ SEGLEN / 20; // 5% + const unsigned meteorSize = 1+ SEGLEN / 20; // 5% uint16_t in = map((SEGENV.step >> 6 & 0xFF), 0, 255, 0, SEGLEN -1); const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; // fade all leds to colors[1] in LEDs one step - for (int i = 0; i < SEGLEN; i++) { + for (unsigned i = 0; i < SEGLEN; i++) { if (/*trail[i] != 0 &&*/ random8() <= 255 - SEGMENT.intensity) { int change = trail[i] + 4 - random8(24); //change each time between -20 and +4 trail[i] = constrain(change, 0, max); @@ -2419,7 +2405,7 @@ uint16_t mode_meteor_smooth() { // draw meteor for (unsigned j = 0; j < meteorSize; j++) { - uint16_t index = in + j; + unsigned index = in + j; if (index >= SEGLEN) { index -= SEGLEN; } @@ -2437,7 +2423,7 @@ static const char _data_FX_MODE_METEOR_SMOOTH[] PROGMEM = "Meteor Smooth@!,Trail //Railway Crossing / Christmas Fairy lights uint16_t mode_railway() { if (SEGLEN == 1) return mode_static(); - uint16_t dur = (256 - SEGMENT.speed) * 40; + unsigned dur = (256 - SEGMENT.speed) * 40; uint16_t rampdur = (dur * SEGMENT.intensity) >> 8; if (SEGENV.step > dur) { @@ -2445,10 +2431,10 @@ uint16_t mode_railway() { SEGENV.step = 0; SEGENV.aux0 = !SEGENV.aux0; } - uint8_t pos = 255; + unsigned pos = 255; if (rampdur != 0) { - uint16_t p0 = (SEGENV.step * 255) / rampdur; + unsigned p0 = (SEGENV.step * 255) / rampdur; if (p0 < 255) pos = p0; } if (SEGENV.aux0) pos = 255 - pos; @@ -2482,43 +2468,42 @@ typedef struct Ripple { #else #define MAX_RIPPLES 100 #endif -uint16_t ripple_base() -{ - uint16_t maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 - uint16_t dataSize = sizeof(ripple) * maxRipples; +static uint16_t ripple_base() { + unsigned maxRipples = min(1 + (SEGLEN >> 2), MAX_RIPPLES); // 56 max for 16 segment ESP8266 + unsigned dataSize = sizeof(ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); //draw wave - for (int i = 0; i < maxRipples; i++) { - uint16_t ripplestate = ripples[i].state; + for (unsigned i = 0; i < maxRipples; i++) { + unsigned ripplestate = ripples[i].state; if (ripplestate) { - uint8_t rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation - uint16_t rippleorigin = ripples[i].pos; + unsigned rippledecay = (SEGMENT.speed >> 4) +1; //faster decay if faster propagation + unsigned rippleorigin = ripples[i].pos; uint32_t col = SEGMENT.color_from_palette(ripples[i].color, false, false, 255); - uint16_t propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); - int16_t propI = propagation >> 8; - uint8_t propF = propagation & 0xFF; - uint8_t amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); + unsigned propagation = ((ripplestate/rippledecay - 1) * (SEGMENT.speed + 1)); + int propI = propagation >> 8; + unsigned propF = propagation & 0xFF; + unsigned amp = (ripplestate < 17) ? triwave8((ripplestate-1)*8) : map(ripplestate,17,255,255,2); #ifndef WLED_DISABLE_2D if (SEGMENT.is2D()) { propI /= 2; - uint16_t cx = rippleorigin >> 8; - uint16_t cy = rippleorigin & 0xFF; - uint8_t mag = scale8(sin8((propF>>2)), amp); + unsigned cx = rippleorigin >> 8; + unsigned cy = rippleorigin & 0xFF; + unsigned mag = scale8(sin8((propF>>2)), amp); if (propI > 0) SEGMENT.drawCircle(cx, cy, propI, color_blend(SEGMENT.getPixelColorXY(cx + propI, cy), col, mag), true); } else #endif { - int16_t left = rippleorigin - propI -1; - for (int16_t v = left; v < left +4; v++) { - uint8_t mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); - SEGMENT.setPixelColor(v, color_blend(SEGMENT.getPixelColor(v), col, mag)); // TODO - int16_t w = left + propI*2 + 3 -(v-left); - SEGMENT.setPixelColor(w, color_blend(SEGMENT.getPixelColor(w), col, mag)); // TODO + int left = rippleorigin - propI -1; + int right = rippleorigin + propI +3; + for (int v = 0; v < 4; v++) { + unsigned mag = scale8(cubicwave8((propF>>2)+(v-left)*64), amp); + SEGMENT.setPixelColor(left + v, color_blend(SEGMENT.getPixelColor(left + v), col, mag)); // TODO + SEGMENT.setPixelColor(right - v, color_blend(SEGMENT.getPixelColor(right - v), col, mag)); // TODO } } ripplestate += rippledecay; @@ -2569,24 +2554,24 @@ static const char _data_FX_MODE_RIPPLE_RAINBOW[] PROGMEM = "Ripple Rainbow@!,Wav // // TwinkleFOX: Twinkling 'holiday' lights that fade in and out. // Colors are chosen from a palette. Read more about this effect using the link above! -CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) +static CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) - uint16_t ticks = ms / SEGENV.aux0; - uint8_t fastcycle8 = ticks; - uint16_t slowcycle16 = (ticks >> 8) + salt; + unsigned ticks = ms / SEGENV.aux0; + unsigned fastcycle8 = ticks; + unsigned slowcycle16 = (ticks >> 8) + salt; slowcycle16 += sin8(slowcycle16); slowcycle16 = (slowcycle16 * 2053) + 1384; - uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); + unsigned slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); // Overall twinkle density. // 0 (NONE lit) to 8 (ALL lit at once). // Default is 5. - uint8_t twinkleDensity = (SEGMENT.intensity >> 5) +1; + unsigned twinkleDensity = (SEGMENT.intensity >> 5) +1; - uint8_t bright = 0; + unsigned bright = 0; if (((slowcycle8 & 0x0E)/2) < twinkleDensity) { - uint8_t ph = fastcycle8; + unsigned ph = fastcycle8; // This is like 'triwave8', which produces a // symmetrical up-and-down triangle sawtooth waveform, except that this // function produces a triangle wave with a faster attack and a slower decay @@ -2603,7 +2588,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) } } - uint8_t hue = slowcycle8 - salt; + unsigned hue = slowcycle8 - salt; CRGB c; if (bright > 0) { c = ColorFromPalette(SEGPALETTE, hue, bright, NOBLEND); @@ -2613,7 +2598,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // way that incandescent bulbs fade toward 'red' as they dim. if (fastcycle8 >= 128) { - uint8_t cooling = (fastcycle8 - 128) >> 4; + unsigned cooling = (fastcycle8 - 128) >> 4; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } @@ -2629,7 +2614,7 @@ CRGB twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) // "CalculateOneTwinkle" on each pixel. It then displays // either the twinkle color of the background color, // whichever is brighter. -uint16_t twinklefox_base(bool cat) +static uint16_t twinklefox_base(bool cat) { // "PRNG16" is the pseudorandom number generator // It MUST be reset to the same starting value each time @@ -2643,7 +2628,7 @@ uint16_t twinklefox_base(bool cat) // Set up the background color, "bg". CRGB bg = CRGB(SEGCOLOR(1)); - uint8_t bglight = bg.getAverageLight(); + unsigned bglight = bg.getAverageLight(); if (bglight > 64) { bg.nscale8_video(16); // very bright, so scale to 1/16th } else if (bglight > 16) { @@ -2652,25 +2637,25 @@ uint16_t twinklefox_base(bool cat) bg.nscale8_video(86); // dim, scale to 1/3rd. } - uint8_t backgroundBrightness = bg.getAverageLight(); + unsigned backgroundBrightness = bg.getAverageLight(); for (int i = 0; i < SEGLEN; i++) { PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number - uint16_t myclockoffset16= PRNG16; // use that number as clock offset + unsigned myclockoffset16= PRNG16; // use that number as clock offset PRNG16 = (uint16_t)(PRNG16 * 2053) + 1384; // next 'random' number // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths) - uint8_t myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; + unsigned myspeedmultiplierQ5_3 = ((((PRNG16 & 0xFF)>>4) + (PRNG16 & 0x0F)) & 0x0F) + 0x08; uint32_t myclock30 = (uint32_t)((strip.now * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; - uint8_t myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel + unsigned myunique8 = PRNG16 >> 8; // get 'salt' value for this pixel // We now have the adjusted 'clock' for this pixel, now we call // the function that computes what color the pixel should be based // on the "brightness = f( time )" idea. CRGB c = twinklefox_one_twinkle(myclock30, myunique8, cat); - uint8_t cbright = c.getAverageLight(); - int16_t deltabright = cbright - backgroundBrightness; + unsigned cbright = c.getAverageLight(); + int deltabright = cbright - backgroundBrightness; if (deltabright >= 32 || (!bg)) { // If the new pixel is significantly brighter than the background color, // use the new color. @@ -2726,10 +2711,10 @@ uint16_t mode_halloween_eyes() }; if (SEGLEN == 1) return mode_static(); - const uint16_t maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; - const uint16_t HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); - const uint16_t HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; - uint16_t eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; + const unsigned maxWidth = strip.isMatrix ? SEGMENT.virtualWidth() : SEGLEN; + const unsigned HALLOWEEN_EYE_SPACE = MAX(2, strip.isMatrix ? SEGMENT.virtualWidth()>>4: SEGLEN>>5); + const unsigned HALLOWEEN_EYE_WIDTH = HALLOWEEN_EYE_SPACE/2; + unsigned eyeLength = (2*HALLOWEEN_EYE_WIDTH) + HALLOWEEN_EYE_SPACE; if (eyeLength >= maxWidth) return mode_static(); //bail if segment too short if (!SEGENV.allocateData(sizeof(EyeData))) return mode_static(); //allocation failed @@ -2738,7 +2723,7 @@ uint16_t mode_halloween_eyes() if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); //fill background data.state = static_cast(data.state % eyeState::count); - uint16_t duration = max(uint16_t{1u}, data.duration); + unsigned duration = max(uint16_t{1u}, data.duration); const uint32_t elapsedTime = strip.now - data.startTime; switch (data.state) { @@ -2763,9 +2748,9 @@ uint16_t mode_halloween_eyes() // - randomly switch to the blink (sub-)state, and initialize it with a blink duration (more precisely, a blink end time stamp) // - never switch to the blink state if the animation just started or is about to end - uint16_t start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; + unsigned start2ndEye = data.startPos + HALLOWEEN_EYE_WIDTH + HALLOWEEN_EYE_SPACE; // If the user reduces the input while in this state, limit the duration. - duration = min(duration, static_cast(128u + (SEGMENT.intensity * 64u))); + duration = min(duration, (128u + (SEGMENT.intensity * 64u))); constexpr uint32_t minimumOnTimeBegin = 1024u; constexpr uint32_t minimumOnTimeEnd = 1024u; @@ -2789,10 +2774,10 @@ uint16_t mode_halloween_eyes() if (c != backgroundColor) { // render eyes - for (int i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { + for (unsigned i = 0; i < HALLOWEEN_EYE_WIDTH; i++) { if (strip.isMatrix) { - SEGMENT.setPixelColorXY(data.startPos + i, SEGMENT.offset, c); - SEGMENT.setPixelColorXY(start2ndEye + i, SEGMENT.offset, c); + SEGMENT.setPixelColorXY(data.startPos + i, (unsigned)SEGMENT.offset, c); + SEGMENT.setPixelColorXY(start2ndEye + i, (unsigned)SEGMENT.offset, c); } else { SEGMENT.setPixelColor(data.startPos + i, c); SEGMENT.setPixelColor(start2ndEye + i, c); @@ -2815,7 +2800,7 @@ uint16_t mode_halloween_eyes() // - select a duration // - immediately switch to eyes-off state - const uint16_t eyeOffTimeBase = SEGMENT.speed*128u; + const unsigned eyeOffTimeBase = SEGMENT.speed*128u; duration = eyeOffTimeBase + random16(eyeOffTimeBase); data.duration = duration; data.state = eyeState::off; @@ -2826,8 +2811,8 @@ uint16_t mode_halloween_eyes() // - not much to do here // If the user reduces the input while in this state, limit the duration. - const uint16_t eyeOffTimeBase = SEGMENT.speed*128u; - duration = min(duration, static_cast(2u * eyeOffTimeBase)); + const unsigned eyeOffTimeBase = SEGMENT.speed*128u; + duration = min(duration, (2u * eyeOffTimeBase)); break; } case eyeState::count: { @@ -2863,10 +2848,10 @@ static const char _data_FX_MODE_HALLOWEEN_EYES[] PROGMEM = "Halloween Eyes@Eye o //Speed slider sets amount of LEDs lit, intensity sets unlit uint16_t mode_static_pattern() { - uint16_t lit = 1 + SEGMENT.speed; - uint16_t unlit = 1 + SEGMENT.intensity; + unsigned lit = 1 + SEGMENT.speed; + unsigned unlit = 1 + SEGMENT.intensity; bool drawingLit = true; - uint16_t cnt = 0; + unsigned cnt = 0; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, (drawingLit) ? SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0) : SEGCOLOR(1)); @@ -2884,9 +2869,9 @@ static const char _data_FX_MODE_STATIC_PATTERN[] PROGMEM = "Solid Pattern@Fg siz uint16_t mode_tri_static_pattern() { - uint8_t segSize = (SEGMENT.intensity >> 5) +1; - uint8_t currSeg = 0; - uint16_t currSegCount = 0; + unsigned segSize = (SEGMENT.intensity >> 5) +1; + unsigned currSeg = 0; + unsigned currSegCount = 0; for (int i = 0; i < SEGLEN; i++) { if ( currSeg % 3 == 0 ) { @@ -2894,7 +2879,7 @@ uint16_t mode_tri_static_pattern() } else if( currSeg % 3 == 1) { SEGMENT.setPixelColor(i, SEGCOLOR(1)); } else { - SEGMENT.setPixelColor(i, (SEGCOLOR(2) > 0 ? SEGCOLOR(2) : WHITE)); + SEGMENT.setPixelColor(i, SEGCOLOR(2)); } currSegCount += 1; if (currSegCount >= segSize) { @@ -2908,25 +2893,25 @@ uint16_t mode_tri_static_pattern() static const char _data_FX_MODE_TRI_STATIC_PATTERN[] PROGMEM = "Solid Pattern Tri@,Size;1,2,3;;;pal=0"; -uint16_t spots_base(uint16_t threshold) +static uint16_t spots_base(uint16_t threshold) { if (SEGLEN == 1) return mode_static(); if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - uint16_t maxZones = SEGLEN >> 2; - uint16_t zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); - uint16_t zoneLen = SEGLEN / zones; - uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; + unsigned maxZones = SEGLEN >> 2; + unsigned zones = 1 + ((SEGMENT.intensity * maxZones) >> 8); + unsigned zoneLen = SEGLEN / zones; + unsigned offset = (SEGLEN - zones * zoneLen) >> 1; - for (int z = 0; z < zones; z++) + for (unsigned z = 0; z < zones; z++) { - uint16_t pos = offset + z * zoneLen; - for (int i = 0; i < zoneLen; i++) + unsigned pos = offset + z * zoneLen; + for (unsigned i = 0; i < zoneLen; i++) { - uint16_t wave = triwave16((i * 0xFFFF) / zoneLen); + unsigned wave = triwave16((i * 0xFFFF) / zoneLen); if (wave > threshold) { - uint16_t index = 0 + pos + i; - uint8_t s = (wave - threshold)*255 / (0xFFFF - threshold); + unsigned index = 0 + pos + i; + unsigned s = (wave - threshold)*255 / (0xFFFF - threshold); SEGMENT.setPixelColor(index, color_blend(SEGMENT.color_from_palette(index, true, PALETTE_SOLID_WRAP, 0), SEGCOLOR(1), 255-s)); } } @@ -2947,14 +2932,14 @@ static const char _data_FX_MODE_SPOTS[] PROGMEM = "Spots@Spread,Width,,,,,Overla //Intensity slider sets number of "lights", LEDs per light fade in and out uint16_t mode_spots_fade() { - uint16_t counter = strip.now * ((SEGMENT.speed >> 2) +8); - uint16_t t = triwave16(counter); - uint16_t tr = (t >> 1) + (t >> 2); + unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); + unsigned t = triwave16(counter); + unsigned tr = (t >> 1) + (t >> 2); return spots_base(tr); } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -2965,12 +2950,13 @@ typedef struct Ball { /* * Bouncing Balls Effect */ + uint16_t mode_bouncing_balls(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - const uint16_t strips = SEGMENT.nrOfVStrips(); // adapt for 2D + const unsigned strips = SEGMENT.nrOfVStrips(); // adapt for 2D const size_t maxNumBalls = 16; - uint16_t dataSize = sizeof(ball) * maxNumBalls; + unsigned dataSize = sizeof(ball) * maxNumBalls; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Ball* balls = reinterpret_cast(SEGENV.data); @@ -2984,7 +2970,7 @@ uint16_t mode_bouncing_balls(void) { static void runStrip(size_t stripNr, Ball* balls) { // number of balls based on intensity setting to max of 7 (cycles colors) // non-chosen color is a random color - uint16_t numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball + unsigned numBalls = (SEGMENT.intensity * (maxNumBalls - 1)) / 255 + 1; // minimum 1 ball const float gravity = -9.81f; // standard value of gravity const bool hasCol2 = SEGCOLOR(2); const unsigned long time = strip.now; @@ -3031,7 +3017,7 @@ uint16_t mode_bouncing_balls(void) { } }; - for (int stripNr=0; stripNr pos ; i--) { + for (unsigned i = SEGENV.aux0; i > pos ; i--) { SEGMENT.setPixelColor(i, color1); if (dual) SEGMENT.setPixelColor(SEGLEN-1-i, color2); } @@ -3233,8 +3220,8 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; - -//each needs 19 bytes +#ifndef DISABLE_1D_PS_REPLACEMENTS +//each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { float pos, posX; @@ -3248,11 +3235,14 @@ typedef struct Spark { * POPCORN * modified from https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Popcorn.h */ + uint16_t mode_popcorn(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t strips = SEGMENT.nrOfVStrips(); - uint16_t dataSize = sizeof(spark) * maxNumPopcorn; + unsigned strips = SEGMENT.nrOfVStrips(); + unsigned usablePopcorns = maxNumPopcorn; + if (usablePopcorns * strips * sizeof(spark) > FAIR_DATA_PER_SEG) usablePopcorns = FAIR_DATA_PER_SEG / (strips * sizeof(spark)) + 1; // at least 1 popcorn per vstrip + unsigned dataSize = sizeof(spark) * usablePopcorns; // on a matrix 64x64 this could consume a little less than 27kB when Bar expansion is used if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); @@ -3261,14 +3251,14 @@ uint16_t mode_popcorn(void) { if (!SEGMENT.check2) SEGMENT.fill(hasCol2 ? BLACK : SEGCOLOR(1)); struct virtualStrip { - static void runStrip(uint16_t stripNr, Spark* popcorn) { - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s + static void runStrip(uint16_t stripNr, Spark* popcorn, unsigned usablePopcorns) { + float gravity = -0.0001f - (SEGMENT.speed/200000.0f); // m/s/s gravity *= SEGLEN; - uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; + unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(int i = 0; i < numPopcorn; i++) { + for(unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; @@ -3276,7 +3266,7 @@ uint16_t mode_popcorn(void) { if (random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; - uint16_t peakHeight = 128 + random8(128); //0-255 + unsigned peakHeight = 128 + random8(128); //0-255 peakHeight = (peakHeight * (SEGLEN -1)) >> 8; popcorn[i].vel = sqrtf(-2.0f * gravity * peakHeight); @@ -3293,20 +3283,21 @@ uint16_t mode_popcorn(void) { if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) uint32_t col = SEGMENT.color_wheel(popcorn[i].colIndex); if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); - uint16_t ledIndex = popcorn[i].pos; + unsigned ledIndex = popcorn[i].pos; if (ledIndex < SEGLEN) SEGMENT.setPixelColor(indexToVStrip(ledIndex, stripNr), col); } } } }; - for (int stripNr=0; stripNr 1) { //allocate segment data - uint16_t dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) + unsigned dataSize = max(1, SEGLEN -1) *3; //max. 1365 pixels (ESP8266) if (!SEGENV.allocateData(dataSize)) return candle(false); //allocation failed } //max. flicker range controlled by intensity - uint8_t valrange = SEGMENT.intensity; - uint8_t rndval = valrange >> 1; //max 127 + unsigned valrange = SEGMENT.intensity; + unsigned rndval = valrange >> 1; //max 127 //step (how much to move closer to target per frame) coarsely set by speed - uint8_t speedFactor = 4; + unsigned speedFactor = 4; if (SEGMENT.speed > 252) { //epilepsy speedFactor = 1; } else if (SEGMENT.speed > 99) { //regular candle (mode called every ~25 ms, so 4 frames to have a new target every 100ms) @@ -3334,13 +3325,13 @@ uint16_t candle(bool multi) speedFactor = 3; } //else 4 (slowest) - uint16_t numCandles = (multi) ? SEGLEN : 1; + unsigned numCandles = (multi) ? SEGLEN : 1; - for (int i = 0; i < numCandles; i++) + for (unsigned i = 0; i < numCandles; i++) { - uint16_t d = 0; //data location + unsigned d = 0; //data location - uint8_t s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; + unsigned s = SEGENV.aux0, s_target = SEGENV.aux1, fadeStep = SEGENV.step; if (i > 0) { d = (i-1) *3; s = SEGENV.data[d]; s_target = SEGENV.data[d+1]; fadeStep = SEGENV.data[d+2]; @@ -3361,16 +3352,16 @@ uint16_t candle(bool multi) if (newTarget) { s_target = random8(rndval) + random8(rndval); //between 0 and rndval*2 -2 = 252 if (s_target < (rndval >> 1)) s_target = (rndval >> 1) + random8(rndval); - uint8_t offset = (255 - valrange); + unsigned offset = (255 - valrange); s_target += offset; - uint8_t dif = (s_target > s) ? s_target - s : s - s_target; + unsigned dif = (s_target > s) ? s_target - s : s - s_target; fadeStep = dif >> speedFactor; if (fadeStep == 0) fadeStep = 1; } - if (i > 0) { + if (i > 0) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), s)); SEGENV.data[d] = s; SEGENV.data[d+1] = s_target; SEGENV.data[d+2] = fadeStep; @@ -3400,7 +3391,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifndef DISABLE_1D_PS_REPLACEMENTS /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3423,15 +3414,15 @@ typedef struct particle { uint16_t mode_starburst(void) { if (SEGLEN == 1) return mode_static(); - uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = strip.getActiveSegmentsNum(); + unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 + unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs - uint16_t maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg + unsigned maxStars = maxData / sizeof(star); //ESP8266: max. 4/9/19 stars/seg, ESP32: max. 10/21/42 stars/seg - uint8_t numStars = 1 + (SEGLEN >> 3); + unsigned numStars = 1 + (SEGLEN >> 3); if (numStars > maxStars) numStars = maxStars; - uint16_t dataSize = sizeof(star) * numStars; + unsigned dataSize = sizeof(star) * numStars; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -3443,18 +3434,18 @@ uint16_t mode_starburst(void) { float particleIgnition = 250.0f; // How long to "flash" float particleFadeTime = 1500.0f; // Fade out time - for (int j = 0; j < numStars; j++) + for (unsigned j = 0; j < numStars; j++) { // speed to adjust chance of a burst, max is nearly always. if (random8((144-(SEGMENT.speed >> 1))) == 0 && stars[j].birth == 0) { // Pick a random color and location. - uint16_t startPos = (SEGLEN > 1) ? random16(SEGLEN-1) : 0; - float multiplier = (float)(random8())/255.0 * 1.0; + unsigned startPos = random16(SEGLEN-1); + float multiplier = (float)(random8())/255.0f * 1.0f; stars[j].color = CRGB(SEGMENT.color_wheel(random8())); stars[j].pos = startPos; - stars[j].vel = maxSpeed * (float)(random8())/255.0 * multiplier; + stars[j].vel = maxSpeed * (float)(random8())/255.0f * multiplier; stars[j].birth = it; stars[j].last = it; // more fragments means larger burst effect @@ -3469,7 +3460,7 @@ uint16_t mode_starburst(void) { if (!SEGMENT.check2) SEGMENT.fill(SEGCOLOR(1)); - for (int j=0; j> 1; + unsigned i = index >> 1; if (stars[j].fragment[i] > 0) { float loc = stars[j].fragment[i]; if (mirrored) loc -= (loc-stars[j].pos)*2; @@ -3534,7 +3525,6 @@ uint16_t mode_starburst(void) { #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; - /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3543,18 +3533,18 @@ static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chanc uint16_t mode_exploding_fireworks(void) { if (SEGLEN == 1) return mode_static(); - const uint16_t cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const int cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; + const int rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //allocate segment data - uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 - uint8_t segs = strip.getActiveSegmentsNum(); + unsigned maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 + unsigned segs = strip.getActiveSegmentsNum(); if (segs <= (strip.getMaxSegments() /2)) maxData *= 2; //ESP8266: 512 if <= 8 segs ESP32: 1280 if <= 16 segs if (segs <= (strip.getMaxSegments() /4)) maxData *= 2; //ESP8266: 1024 if <= 4 segs ESP32: 2560 if <= 8 segs int maxSparks = maxData / sizeof(spark); //ESP8266: max. 21/42/85 sparks/seg, ESP32: max. 53/106/213 sparks/seg - uint16_t numSparks = min(2 + ((rows*cols) >> 1), maxSparks); - uint16_t dataSize = sizeof(spark) * numSparks; + unsigned numSparks = min(2 + ((rows*cols) >> 1), maxSparks); + unsigned dataSize = sizeof(spark) * numSparks; if (!SEGENV.allocateData(dataSize + sizeof(float))) return mode_static(); //allocation failed float *dying_gravity = reinterpret_cast(SEGENV.data + dataSize); @@ -3576,7 +3566,7 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D - uint16_t peakHeight = 75 + random8(180); //0-255 + unsigned peakHeight = 75 + random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D @@ -3587,7 +3577,7 @@ uint16_t mode_exploding_fireworks(void) // launch if (flare->vel > 12 * gravity) { // flare - if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(unsigned(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); else SEGMENT.setPixelColor((flare->posX > 0.0f) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); flare->pos += flare->vel; flare->pos = constrain(flare->pos, 0, rows-1); @@ -3607,12 +3597,12 @@ uint16_t mode_exploding_fireworks(void) * Explosion happens where the flare ended. * Size is proportional to the height. */ - int nSparks = flare->pos + random8(4); + unsigned nSparks = flare->pos + random8(4); nSparks = constrain(nSparks, 4, numSparks); // initialize sparks if (SEGENV.aux0 == 2) { - for (int i = 1; i < nSparks; i++) { + for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos = flare->pos; sparks[i].posX = flare->posX; sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 @@ -3631,7 +3621,7 @@ uint16_t mode_exploding_fireworks(void) } if (sparks[1].col > 4) {//&& sparks[1].pos > 0) { // as long as our known spark is lit, work with all the sparks - for (int i = 1; i < nSparks; i++) { + for (unsigned i = 1; i < nSparks; i++) { sparks[i].pos += sparks[i].vel; sparks[i].posX += sparks[i].velX; sparks[i].vel += *dying_gravity; @@ -3640,14 +3630,14 @@ uint16_t mode_exploding_fireworks(void) if (sparks[i].pos > 0 && sparks[i].pos < rows) { if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; - uint16_t prog = sparks[i].col; + unsigned prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); CRGB c = CRGB::Black; //HeatColor(sparks[i].col); if (prog > 300) { //fade from white to spark color c = CRGB(color_blend(spColor, WHITE, (prog - 300)*5)); } else if (prog > 45) { //fade from spark color to black c = CRGB(color_blend(BLACK, spColor, prog - 45)); - uint8_t cooling = (300 - prog) >> 5; + unsigned cooling = (300 - prog) >> 5; c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } @@ -3672,7 +3662,6 @@ uint16_t mode_exploding_fireworks(void) #undef MAX_SPARKS static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; - /* * Drip Effect * ported of: https://www.youtube.com/watch?v=sru2fXh4r7k @@ -3681,9 +3670,9 @@ uint16_t mode_drip(void) { if (SEGLEN == 1) return mode_static(); //allocate segment data - uint16_t strips = SEGMENT.nrOfVStrips(); + unsigned strips = SEGMENT.nrOfVStrips(); const int maxNumDrops = 4; - uint16_t dataSize = sizeof(spark) * maxNumDrops; + unsigned dataSize = sizeof(spark) * maxNumDrops; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* drops = reinterpret_cast(SEGENV.data); @@ -3692,13 +3681,13 @@ uint16_t mode_drip(void) struct virtualStrip { static void runStrip(uint16_t stripNr, Spark* drops) { - uint8_t numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 + unsigned numDrops = 1 + (SEGMENT.intensity >> 6); // 255>>6 = 3 - float gravity = -0.0005 - (SEGMENT.speed/50000.0); + float gravity = -0.0005f - (SEGMENT.speed/50000.0f); gravity *= max(1, SEGLEN-1); int sourcedrop = 12; - for (int j=0;j= SEGLEN occasionally + unsigned pos = constrain(uint16_t(drops[j].pos) +i, 0, SEGLEN-1); //this is BAD, returns a pos >= SEGLEN occasionally SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color_blend(BLACK,SEGCOLOR(0),drops[j].col/i)); //spread pixel with fade while falling } @@ -3752,14 +3741,13 @@ uint16_t mode_drip(void) } }; - for (int stripNr=0; stripNr(SEGENV.data); @@ -3843,7 +3831,7 @@ uint16_t mode_tetrix(void) { } }; - for (int stripNr=0; stripNr> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. - + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. - uint8_t thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); - //CRGB color = ColorFromPalette(SEGPALETTE, colorIndex, thisBright, LINEARBLEND); - //SEGMENT.setPixelColor(i, color.red, color.green, color.blue); + for (unsigned i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set color & brightness based on a wave as follows: + unsigned colorIndex = cubicwave8((i*(2+ 3*(SEGMENT.speed >> 5))+thisPhase) & 0xFF)/2 // factor=23 // Create a wave and add a phase change and add another wave with its own phase change. + + cos8((i*(1+ 2*(SEGMENT.speed >> 5))+thatPhase) & 0xFF)/2; // factor=15 // Hey, you can even change the frequencies if you wish. + unsigned thisBright = qsub8(colorIndex, beatsin8(7,0, (128 - (SEGMENT.intensity>>1)))); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0, thisBright)); } @@ -3883,12 +3869,12 @@ static const char _data_FX_MODE_PLASMA[] PROGMEM = "Plasma@Phase,!;!;!"; */ uint16_t mode_percent(void) { - uint8_t percent = SEGMENT.intensity; + unsigned percent = SEGMENT.intensity; percent = constrain(percent, 0, 200); - uint16_t active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) + unsigned active_leds = (percent < 100) ? roundf(SEGLEN * percent / 100.0f) : roundf(SEGLEN * (200 - percent) / 100.0f); - uint8_t size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); + unsigned size = (1 + ((SEGMENT.speed * SEGLEN) >> 11)); if (SEGMENT.speed == 255) size = 255; if (percent <= 100) { @@ -3935,7 +3921,7 @@ static const char _data_FX_MODE_PERCENT[] PROGMEM = "Percent@,% of fill,,,,One c * (unimplemented?) tries to draw an ECG approximation on a 2D matrix */ uint16_t mode_heartbeat(void) { - uint8_t bpm = 40 + (SEGMENT.speed >> 3); + unsigned bpm = 40 + (SEGMENT.speed >> 3); uint32_t msPerBeat = (60000L / bpm); uint32_t secondBeat = (msPerBeat / 3); uint32_t bri_lower = SEGENV.aux1; @@ -3988,18 +3974,18 @@ static const char _data_FX_MODE_HEARTBEAT[] PROGMEM = "Heartbeat@!,!;!,!;!;01;m1 // Modified for WLED, based on https://github.com/FastLED/FastLED/blob/master/examples/Pacifica/Pacifica.ino // // Add one layer of waves into the led array -CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) +static CRGB pacifica_one_layer(uint16_t i, CRGBPalette16& p, uint16_t cistart, uint16_t wavescale, uint8_t bri, uint16_t ioff) { - uint16_t ci = cistart; - uint16_t waveangle = ioff; - uint16_t wavescale_half = (wavescale >> 1) + 20; + unsigned ci = cistart; + unsigned waveangle = ioff; + unsigned wavescale_half = (wavescale >> 1) + 20; waveangle += ((120 + SEGMENT.intensity) * i); //original 250 * i - uint16_t s16 = sin16(waveangle) + 32768; - uint16_t cs = scale16(s16, wavescale_half) + wavescale_half; + unsigned s16 = sin16(waveangle) + 32768; + unsigned cs = scale16(s16, wavescale_half) + wavescale_half; ci += (cs * i); - uint16_t sindex16 = sin16(ci) + 32768; - uint8_t sindex8 = scale16(sindex16, 240); + unsigned sindex16 = sin16(ci) + 32768; + unsigned sindex8 = scale16(sindex16, 240); return ColorFromPalette(p, sindex8, bri, LINEARBLEND); } @@ -4025,13 +4011,13 @@ uint16_t mode_pacifica() // Increment the four "color index start" counters, one for each wave layer. // Each is incremented at a different speed, and the speeds vary over time. - uint16_t sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; + unsigned sCIStart1 = SEGENV.aux0, sCIStart2 = SEGENV.aux1, sCIStart3 = SEGENV.step, sCIStart4 = SEGENV.step >> 16; uint32_t deltams = (FRAMETIME >> 2) + ((FRAMETIME * SEGMENT.speed) >> 7); uint64_t deltat = (strip.now >> 2) + ((strip.now * SEGMENT.speed) >> 7); strip.now = deltat; - uint16_t speedfactor1 = beatsin16(3, 179, 269); - uint16_t speedfactor2 = beatsin16(4, 179, 269); + unsigned speedfactor1 = beatsin16(3, 179, 269); + unsigned speedfactor2 = beatsin16(4, 179, 269); uint32_t deltams1 = (deltams * speedfactor1) / 256; uint32_t deltams2 = (deltams * speedfactor2) / 256; uint32_t deltams21 = (deltams1 + deltams2) / 2; @@ -4045,8 +4031,8 @@ uint16_t mode_pacifica() // Clear out the LED array to a dim background blue-green //SEGMENT.fill(132618); - uint8_t basethreshold = beatsin8( 9, 55, 65); - uint8_t wave = beat8( 7 ); + unsigned basethreshold = beatsin8( 9, 55, 65); + unsigned wave = beat8( 7 ); for (int i = 0; i < SEGLEN; i++) { CRGB c = CRGB(2, 6, 10); @@ -4057,12 +4043,12 @@ uint16_t mode_pacifica() c += pacifica_one_layer(i, pacifica_palette_3, sCIStart4, 5 * 256 , beatsin8(8, 10,28) , beat16(601)); // Add extra 'white' to areas where the four layers of light have lined up brightly - uint8_t threshold = scale8( sin8( wave), 20) + basethreshold; + unsigned threshold = scale8( sin8( wave), 20) + basethreshold; wave += 7; - uint8_t l = c.getAverageLight(); + unsigned l = c.getAverageLight(); if (l > threshold) { - uint8_t overage = l - threshold; - uint8_t overage2 = qadd8(overage, overage); + unsigned overage = l - threshold; + unsigned overage2 = qadd8(overage, overage); c += CRGB(overage, overage2, qadd8(overage2, overage2)); } @@ -4095,15 +4081,15 @@ uint16_t mode_sunrise() { } SEGMENT.fill(BLACK); - uint16_t stage = 0xFFFF; + unsigned stage = 0xFFFF; uint32_t s10SinceStart = (millis() - SEGENV.step) /100; //tenths of seconds if (SEGMENT.speed > 120) { //quick sunrise and sunset - uint16_t counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); + unsigned counter = (strip.now >> 1) * (((SEGMENT.speed -120) >> 1) +1); stage = triwave16(counter); } else if (SEGMENT.speed) { //sunrise - uint8_t durMins = SEGMENT.speed; + unsigned durMins = SEGMENT.speed; if (durMins > 60) durMins -= 60; uint32_t s10Target = durMins * 600; if (s10SinceStart > s10Target) s10SinceStart = s10Target; @@ -4116,7 +4102,7 @@ uint16_t mode_sunrise() { //default palette is Fire uint32_t c = SEGMENT.color_from_palette(0, false, true, 255); //background - uint16_t wave = triwave16((i * stage) / SEGLEN); + unsigned wave = triwave16((i * stage) / SEGLEN); wave = (wave >> 8) + ((wave * SEGMENT.intensity) >> 15); @@ -4137,22 +4123,22 @@ static const char _data_FX_MODE_SUNRISE[] PROGMEM = "Sunrise@Time [min],Width;;! /* * Effects by Andrew Tuline */ -uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. +static uint16_t phased_base(uint8_t moder) { // We're making sine waves here. By Andrew Tuline. - uint8_t allfreq = 16; // Base frequency. + unsigned allfreq = 16; // Base frequency. float *phase = reinterpret_cast(&SEGENV.step); // Phase change value gets calculated (float fits into unsigned long). - uint8_t cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). - uint8_t modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). + unsigned cutOff = (255-SEGMENT.intensity); // You can change the number of pixels. AKA INTENSITY (was 192). + unsigned modVal = 5;//SEGMENT.fft1/8+1; // You can change the modulus. AKA FFT1 (was 5). - uint8_t index = strip.now/64; // Set color rotation speed + unsigned index = strip.now/64; // Set color rotation speed *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) for (int i = 0; i < SEGLEN; i++) { if (moder == 1) modVal = (inoise8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. - uint16_t val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. + unsigned val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. - uint8_t b = cubicwave8(val); // Now we make an 8 bit sinewave. + unsigned b = cubicwave8(val); // Now we make an 8 bit sinewave. b = (b > cutOff) ? (b - cutOff) : 0; // A ternary operator to cutoff the light. SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(index, false, false, 0), b)); index += 256 / SEGLEN; @@ -4176,12 +4162,12 @@ static const char _data_FX_MODE_PHASEDNOISE[] PROGMEM = "Phased Noise@!,!;!,!;!" uint16_t mode_twinkleup(void) { // A very short twinkle routine with fade-in and dual controls. By Andrew Tuline. - uint16_t prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function + unsigned prevSeed = random16_get_seed(); // save seed so we can restore it at the end of the function random16_set_seed(535); // The randomizer needs to be re-set each time through the loop in order for the same 'random' numbers to be the same each time through. for (int i = 0; i < SEGLEN; i++) { - uint8_t ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. - uint8_t pixBri = sin8(ranstart + 16 * strip.now/(256-SEGMENT.speed)); + unsigned ranstart = random8(); // The starting value (aka brightness) for each pixel. Must be consistent each time through the loop for this to work. + unsigned pixBri = sin8(ranstart + 16 * strip.now/(256-SEGMENT.speed)); if (random8() > SEGMENT.intensity) pixBri = 0; SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(random8()+strip.now/100, false, PALETTE_SOLID_WRAP, 0), pixBri)); } @@ -4194,20 +4180,20 @@ static const char _data_FX_MODE_TWINKLEUP[] PROGMEM = "Twinkleup@!,Intensity;!,! // Peaceful noise that's slow and with gradually changing palettes. Does not support WLED palettes or default colours or controls. uint16_t mode_noisepal(void) { // Slow noise palette by Andrew Tuline. - uint16_t scale = 15 + (SEGMENT.intensity >> 2); //default was 30 + unsigned scale = 15 + (SEGMENT.intensity >> 2); //default was 30 //#define scale 30 - uint16_t dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) + unsigned dataSize = sizeof(CRGBPalette16) * 2; //allocate space for 2 Palettes (2 * 16 * 3 = 96 bytes) if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed CRGBPalette16* palettes = reinterpret_cast(SEGENV.data); - uint16_t changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec + unsigned changePaletteMs = 4000 + SEGMENT.speed *10; //between 4 - 6.5sec if (strip.now - SEGENV.step > changePaletteMs) { SEGENV.step = strip.now; - uint8_t baseI = random8(); + unsigned baseI = random8(); palettes[1] = CRGBPalette16(CHSV(baseI+random8(64), 255, random8(128,255)), CHSV(baseI+128, 255, random8(128,255)), CHSV(baseI+random8(92), 192, random8(128,255)), CHSV(baseI+random8(92), 255, random8(128,255))); } @@ -4219,7 +4205,7 @@ uint16_t mode_noisepal(void) { // Slow noise if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; for (int i = 0; i < SEGLEN; i++) { - uint8_t index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. + unsigned index = inoise8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. color = ColorFromPalette(palettes[0], index, 255, LINEARBLEND); // Use the my own palette. SEGMENT.setPixelColor(i, color.red, color.green, color.blue); } @@ -4237,10 +4223,10 @@ static const char _data_FX_MODE_NOISEPAL[] PROGMEM = "Noise Pal@!,Scale;;!"; uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tuline //#define qsuba(x, b) ((x>b)?x-b:0) // Analog Unsigned subtraction macro. if result <0, then => 0 - uint16_t colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. + unsigned colorIndex = strip.now /32;//(256 - SEGMENT.fft1); // Amount of colour change. SEGENV.step += SEGMENT.speed/16; // Speed of animation. - uint16_t freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. + unsigned freq = SEGMENT.intensity/4;//SEGMENT.fft2/8; // Frequency of the signal. for (int i = 0; i < SEGLEN; i++) { // For each of the LED's in the strand, set a brightness based on a wave as follows: int pixBri = cubicwave8((i*freq)+SEGENV.step);//qsuba(cubicwave8((i*freq)+SEGENV.step), (255-SEGMENT.intensity)); // qsub sets a minimum value called thiscutoff. If < thiscutoff, then bright = 0. Otherwise, bright = 128 (as defined in qsub).. @@ -4250,7 +4236,7 @@ uint16_t mode_sinewave(void) { // Adjustable sinewave. By Andrew Tul return FRAMETIME; } -static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine"; +static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine@!,Scale;;!"; /* @@ -4258,29 +4244,29 @@ static const char _data_FX_MODE_SINEWAVE[] PROGMEM = "Sine"; */ uint16_t mode_flow(void) { - uint16_t counter = 0; + unsigned counter = 0; if (SEGMENT.speed != 0) { counter = strip.now * ((SEGMENT.speed >> 2) +1); counter = counter >> 8; } - uint16_t maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs - uint16_t zones = (SEGMENT.intensity * maxZones) >> 8; + unsigned maxZones = SEGLEN / 6; //only looks good if each zone has at least 6 LEDs + unsigned zones = (SEGMENT.intensity * maxZones) >> 8; if (zones & 0x01) zones++; //zones must be even if (zones < 2) zones = 2; - uint16_t zoneLen = SEGLEN / zones; - uint16_t offset = (SEGLEN - zones * zoneLen) >> 1; + unsigned zoneLen = SEGLEN / zones; + unsigned offset = (SEGLEN - zones * zoneLen) >> 1; SEGMENT.fill(SEGMENT.color_from_palette(-counter, false, true, 255)); - for (int z = 0; z < zones; z++) + for (unsigned z = 0; z < zones; z++) { - uint16_t pos = offset + z * zoneLen; - for (int i = 0; i < zoneLen; i++) + unsigned pos = offset + z * zoneLen; + for (unsigned i = 0; i < zoneLen; i++) { - uint8_t colorIndex = (i * 255 / zoneLen) - counter; - uint16_t led = (z & 0x01) ? i : (zoneLen -1) -i; + unsigned colorIndex = (i * 255 / zoneLen) - counter; + unsigned led = (z & 0x01) ? i : (zoneLen -1) -i; if (SEGMENT.reverse) led = (zoneLen -1) -led; SEGMENT.setPixelColor(pos + led, SEGMENT.color_from_palette(colorIndex, false, true, 255)); } @@ -4299,34 +4285,23 @@ uint16_t mode_chunchun(void) { if (SEGLEN == 1) return mode_static(); SEGMENT.fade_out(254); // add a bit of trail - uint16_t counter = strip.now * (6 + (SEGMENT.speed >> 4)); - uint16_t numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment - uint16_t span = (SEGMENT.intensity << 8) / numBirds; + unsigned counter = strip.now * (6 + (SEGMENT.speed >> 4)); + unsigned numBirds = 2 + (SEGLEN >> 3); // 2 + 1/8 of a segment + unsigned span = (SEGMENT.intensity << 8) / numBirds; - for (int i = 0; i < numBirds; i++) + for (unsigned i = 0; i < numBirds; i++) { counter -= span; - uint16_t megumin = sin16(counter) + 0x8000; - uint16_t bird = uint32_t(megumin * SEGLEN) >> 16; + unsigned megumin = sin16(counter) + 0x8000; + unsigned bird = uint32_t(megumin * SEGLEN) >> 16; uint32_t c = SEGMENT.color_from_palette((i * 255)/ numBirds, false, false, 0); // no palette wrapping - bird = constrain(bird, 0, SEGLEN-1); + bird = constrain(bird, 0U, SEGLEN-1U); SEGMENT.setPixelColor(bird, c); } return FRAMETIME; } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4340,6 +4315,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifndef DISABLE_1D_PS_REPLACEMENTS +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4347,14 +4333,15 @@ typedef struct Spotlight { * * By Steve Pomeroy @xxv */ + uint16_t mode_dancing_shadows(void) { if (SEGLEN == 1) return mode_static(); - uint8_t numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 + unsigned numSpotlights = map(SEGMENT.intensity, 0, 255, 2, SPOT_MAX_COUNT); // 49 on 32 segment ESP32, 17 on 16 segment ESP8266 bool initialize = SEGENV.aux0 != numSpotlights; SEGENV.aux0 = numSpotlights; - uint16_t dataSize = sizeof(spotlight) * numSpotlights; + unsigned dataSize = sizeof(spotlight) * numSpotlights; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Spotlight* spotlights = reinterpret_cast(SEGENV.data); @@ -4366,7 +4353,7 @@ uint16_t mode_dancing_shadows(void) for (size_t i = 0; i < numSpotlights; i++) { if (!initialize) { // advance the position of the spotlight - int16_t delta = (float)(time - spotlights[i].lastUpdateTime) * + int delta = (float)(time - spotlights[i].lastUpdateTime) * (spotlights[i].speed * ((1.0 + SEGMENT.speed)/100.0)); if (abs(delta) >= 1) { @@ -4463,7 +4450,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif //DISABLE_1D_PS_REPLACEMENTS /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -4489,20 +4476,20 @@ static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,! Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e */ uint16_t mode_blends(void) { - uint16_t pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; - uint16_t dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 + unsigned pixelLen = SEGLEN > UINT8_MAX ? UINT8_MAX : SEGLEN; + unsigned dataSize = sizeof(uint32_t) * (pixelLen + 1); // max segment length of 56 pixels on 16 segment ESP8266 if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed uint32_t* pixels = reinterpret_cast(SEGENV.data); - uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); - uint8_t shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; + unsigned blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); + unsigned shift = (strip.now * ((SEGMENT.speed >> 3) +1)) >> 8; - for (int i = 0; i < pixelLen; i++) { + for (unsigned i = 0; i < pixelLen; i++) { pixels[i] = color_blend(pixels[i], SEGMENT.color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); shift += 3; } - uint16_t offset = 0; - for (int i = 0; i < SEGLEN; i++) { + unsigned offset = 0; + for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, pixels[offset++]); if (offset > pixelLen) offset = 0; } @@ -4538,7 +4525,7 @@ typedef struct TvSim { } tvSim; uint16_t mode_tv_simulator(void) { - uint16_t nr, ng, nb, r, g, b, i, hue; + int nr, ng, nb, r, g, b, i, hue; uint8_t sat, bri, j; if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed @@ -4565,7 +4552,7 @@ uint16_t mode_tv_simulator(void) { } // slightly change the color-tone in this sceene - if ( SEGENV.aux0 == 0) { + if (SEGENV.aux0 == 0) { // hue change in both directions j = random8(4 * colorIntensity); hue = (random8() < 128) ? ((j < tvSimulator->sceeneColorHue) ? tvSimulator->sceeneColorHue - j : 767 - tvSimulator->sceeneColorHue - j) : // negative @@ -4814,8 +4801,8 @@ uint16_t mode_perlinmove(void) { if (SEGLEN == 1) return mode_static(); SEGMENT.fade_out(255-SEGMENT.custom1); for (int i = 0; i < SEGMENT.intensity/16 + 1; i++) { - uint16_t locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. - uint16_t pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. + unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + unsigned pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } @@ -4847,8 +4834,8 @@ static const char _data_FX_MODE_WAVESINS[] PROGMEM = "Wavesins@!,Brightness vari ////////////////////////////// // By: ldirko https://editor.soulmatelights.com/gallery/392-flow-led-stripe , modifed by: Andrew Tuline uint16_t mode_FlowStripe(void) { - - const uint16_t hl = SEGLEN * 10 / 13; + if (SEGLEN == 1) return mode_static(); + const int hl = SEGLEN * 10 / 13; uint8_t hue = strip.now / (SEGMENT.speed+1); uint32_t t = strip.now / (SEGMENT.intensity/8+1); @@ -4875,9 +4862,9 @@ static const char _data_FX_MODE_FLOWSTRIPE[] PROGMEM = "Flow Stripe@Hue speed,Ef uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - uint16_t x, y; + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); + int x, y; SEGMENT.fadeToBlackBy(16 + (SEGMENT.speed>>3)); // create fading trails unsigned long t = strip.now/128; // timebase @@ -4896,7 +4883,7 @@ uint16_t mode_2DBlackHole(void) { // By: Stepko https://editor.soulma // central white dot SEGMENT.setPixelColorXY(cols/2, rows/2, WHITE); // blur everything a bit - SEGMENT.blur(16); + SEGMENT.blur(cols*rows > 100 ? 16 : 0); return FRAMETIME; } // mode_2DBlackHole() @@ -4909,8 +4896,8 @@ static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Ou uint16_t mode_2DColoredBursts() { // By: ldirko https://editor.soulmatelights.com/gallery/819-colored-bursts , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGENV.aux0 = 0; // start with red hue @@ -4961,8 +4948,8 @@ static const char _data_FX_MODE_2DCOLOREDBURSTS[] PROGMEM = "Colored Bursts@Spee uint16_t mode_2Ddna(void) { // dna originally by by ldirko at https://pastebin.com/pCkkkzcs. Updated by Preyy. WLED conversion by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(64); for (int i = 0; i < cols; i++) { @@ -4982,31 +4969,31 @@ static const char _data_FX_MODE_2DDNA[] PROGMEM = "DNA@Scroll speed,Blur;;!;2"; uint16_t mode_2DDNASpiral() { // By: ldirko https://editor.soulmatelights.com/gallery/512-dna-spiral-variation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); } - uint8_t speeds = SEGMENT.speed/2 + 7; - uint8_t freq = SEGMENT.intensity/8; + unsigned speeds = SEGMENT.speed/2 + 7; + unsigned freq = SEGMENT.intensity/8; uint32_t ms = strip.now / 20; SEGMENT.fadeToBlackBy(135); for (int i = 0; i < rows; i++) { - uint16_t x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); - uint16_t x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); - uint8_t hue = (i * 128 / rows) + ms; + int x = beatsin8(speeds, 0, cols - 1, 0, i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, i * freq + 128); + int x1 = beatsin8(speeds, 0, cols - 1, 0, 128 + i * freq) + beatsin8(speeds - 7, 0, cols - 1, 0, 128 + 64 + i * freq); + unsigned hue = (i * 128 / rows) + ms; // skip every 4th row every now and then (fade it more) if ((i + ms / 8) & 3) { // draw a gradient line between x and x1 x = x / 2; x1 = x1 / 2; - uint8_t steps = abs8(x - x1) + 1; + unsigned steps = abs8(x - x1) + 1; for (size_t k = 1; k <= steps; k++) { - uint8_t rate = k * 255 / steps; - uint8_t dx = lerp8by8(x, x1, rate); + unsigned rate = k * 255 / steps; + unsigned dx = lerp8by8(x, x1, rate); //SEGMENT.setPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND).nscale8_video(rate)); SEGMENT.addPixelColorXY(dx, i, ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND)); // use setPixelColorXY for different look SEGMENT.fadePixelColorXY(dx, i, rate); @@ -5027,20 +5014,20 @@ static const char _data_FX_MODE_2DDNASPIRAL[] PROGMEM = "DNA Spiral@Scroll speed uint16_t mode_2DDrift() { // By: Stepko https://editor.soulmatelights.com/gallery/884-drift , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - const uint16_t colsCenter = (cols>>1) + (cols%2); - const uint16_t rowsCenter = (rows>>1) + (rows%2); + const int colsCenter = (cols>>1) + (cols%2); + const int rowsCenter = (rows>>1) + (rows%2); SEGMENT.fadeToBlackBy(128); - const uint16_t maxDim = MAX(cols, rows)/2; + const float maxDim = MAX(cols, rows)/2; unsigned long t = strip.now / (32 - (SEGMENT.speed>>3)); unsigned long t_20 = t/20; // softhack007: pre-calculating this gives about 10% speedup for (float i = 1.0f; i < maxDim; i += 0.25f) { float angle = radians(t * (maxDim - i)); - int16_t mySin = sin_t(angle) * i; - int16_t myCos = cos_t(angle) * i; + int mySin = sin_t(angle) * i; + int myCos = cos_t(angle) * i; SEGMENT.setPixelColorXY(colsCenter + mySin, rowsCenter + myCos, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); if (SEGMENT.check1) SEGMENT.setPixelColorXY(colsCenter + myCos, rowsCenter + mySin, ColorFromPalette(SEGPALETTE, (i * 20) + t_20, 255, LINEARBLEND)); } @@ -5057,8 +5044,8 @@ static const char _data_FX_MODE_2DDRIFT[] PROGMEM = "Drift@Rotation speed,Blur a uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline. Yet another short routine. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5091,8 +5078,8 @@ static const char _data_FX_MODE_2DFIRENOISE[] PROGMEM = "Firenoise@X scale,Y sca uint16_t mode_2DFrizzles(void) { // By: Stepko https://editor.soulmatelights.com/gallery/640-color-frizzles , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(16); for (size_t i = 8; i > 0; i--) { @@ -5118,10 +5105,10 @@ typedef struct ColorCount { uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); - const uint16_t dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const uint16_t crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); + const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled + const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed CRGB *prevLeds = reinterpret_cast(SEGENV.data); @@ -5136,7 +5123,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - uint8_t state = random8()%2; + unsigned state = random8()%2; if (state == 0) SEGMENT.setPixelColorXY(x,y, backgroundColor); else @@ -5165,11 +5152,11 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix if (i==0 && j==0) continue; // ignore itself // wrap around segment - int16_t xx = x+i, yy = y+j; + int xx = x+i, yy = y+j; if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; - uint16_t xy = XY(xx, yy); // previous cell xy to check + unsigned xy = XY(xx, yy); // previous cell xy to check // count different neighbours and colors if (prevLeds[xy] != backgroundColor) { neighbors++; @@ -5224,8 +5211,8 @@ static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2 uint16_t mode_2DHiphotic() { // By: ldirko https://editor.soulmatelights.com/gallery/810 , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint32_t a = strip.now / ((SEGMENT.custom3>>1)+1); for (int x = 0; x < cols; x++) { @@ -5256,8 +5243,8 @@ typedef struct Julia { uint16_t mode_2DJulia(void) { // An animated Julia set by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (!SEGENV.allocateData(sizeof(julia))) return mode_static(); Julia* julias = reinterpret_cast(SEGENV.data); @@ -5362,8 +5349,8 @@ static const char _data_FX_MODE_2DJULIA[] PROGMEM = "Julia@,Max iterations per p uint16_t mode_2DLissajous(void) { // By: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(SEGMENT.intensity); uint_fast16_t phase = (strip.now * (1 + SEGENV.custom3)) /32; // allow user to control rotation speed @@ -5390,10 +5377,10 @@ static const char _data_FX_MODE_2DLISSAJOUS[] PROGMEM = "Lissajous@X frequency,F uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. Adapted by Andrew Tuline & improved by merkisoft and ewowi, and softhack007. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails + unsigned dataSize = (SEGMENT.length()+7) >> 3; //1 bit per LED for trails if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed if (SEGENV.call == 0) { @@ -5402,7 +5389,7 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. } uint8_t fade = map(SEGMENT.custom1, 0, 255, 50, 250); // equals trail size - uint8_t speed = (256-SEGMENT.speed) >> map(MIN(rows, 150), 0, 150, 0, 3); // slower speeds for small displays + uint8_t speed = (256-SEGMENT.speed) >> map(min(rows, 150), 0, 150, 0, 3); // slower speeds for small displays uint32_t spawnColor; uint32_t trailColor; @@ -5460,29 +5447,29 @@ static const char _data_FX_MODE_2DMATRIX[] PROGMEM = "Matrix@!,Spawning rate,Tra uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have one of the dimensions be 2 or less. Adapted by Andrew Tuline. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - uint8_t x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - uint8_t y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); - uint8_t x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - uint8_t y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + int x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + int y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function - uint8_t x1 = beatsin8(23 * speed, 0, cols-1); - uint8_t y1 = beatsin8(28 * speed, 0, rows-1); + int x1 = beatsin8(23 * speed, 0, cols-1); + int y1 = beatsin8(28 * speed, 0, rows-1); for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { // calculate distances of the 3 points from actual pixel // and add them together with weightening - uint16_t dx = abs(x - x1); - uint16_t dy = abs(y - y1); - uint16_t dist = 2 * sqrt16((dx * dx) + (dy * dy)); + unsigned dx = abs(x - x1); + unsigned dy = abs(y - y1); + unsigned dist = 2 * sqrt16((dx * dx) + (dy * dy)); dx = abs(x - x2); dy = abs(y - y2); @@ -5493,7 +5480,7 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have dist += sqrt16((dx * dx) + (dy * dy)); // inverse result - byte color = dist ? 1000 / dist : 255; + int color = dist ? 1000 / dist : 255; // map color between thresholds if (color > 0 and color < 60) { @@ -5519,10 +5506,10 @@ static const char _data_FX_MODE_2DMETABALLS[] PROGMEM = "Metaballs@!;;!;2"; uint16_t mode_2Dnoise(void) { // By Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - const uint16_t scale = SEGMENT.intensity+2; + const unsigned scale = SEGMENT.intensity+2; for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { @@ -5542,21 +5529,21 @@ static const char _data_FX_MODE_2DNOISE[] PROGMEM = "Noise2D@!,Scale;;!;2"; uint16_t mode_2DPlasmaball(void) { // By: Stepko https://editor.soulmatelights.com/gallery/659-plasm-ball , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - uint16_t thisVal = inoise8(i * 30, t, t); - uint16_t thisMax = map(thisVal, 0, 255, 0, cols-1); + unsigned thisVal = inoise8(i * 30, t, t); + unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { - uint16_t thisVal_ = inoise8(t, j * 30, t); - uint16_t thisMax_ = map(thisVal_, 0, 255, 0, rows-1); - uint16_t x = (i + thisMax_ - cols / 2); - uint16_t y = (j + thisMax - cols / 2); - uint16_t cx = (i + thisMax_); - uint16_t cy = (j + thisMax); + unsigned thisVal_ = inoise8(t, j * 30, t); + unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); + int x = (i + thisMax_ - cols / 2); + int y = (j + thisMax - cols / 2); + int cx = (i + thisMax_); + int cy = (j + thisMax); SEGMENT.addPixelColorXY(i, j, ((x - y > -2) && (x - y < 2)) || ((cols - 1 - x - y) > -2 && (cols - 1 - x - y < 2)) || @@ -5582,8 +5569,8 @@ static const char _data_FX_MODE_2DPLASMABALL[] PROGMEM = "Plasma Ball@Speed,,Fad uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https://editor.soulmatelights.com/gallery/762-polar-lights , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); CRGBPalette16 auroraPalette = {0x000000, 0x003300, 0x006600, 0x009900, 0x00cc00, 0x00ff00, 0x33ff00, 0x66ff00, 0x99ff00, 0xccff00, 0xffff00, 0xffcc00, 0xff9900, 0xff6600, 0xff3300, 0xff0000}; @@ -5593,7 +5580,7 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https } float adjustHeight = (float)map(rows, 8, 32, 28, 12); // maybe use mapf() ??? - uint16_t adjScale = map(cols, 8, 64, 310, 63); + unsigned adjScale = map(cols, 8, 64, 310, 63); /* if (SEGENV.aux1 != SEGMENT.custom1/12) { // Hacky palette rotation. We need that black. SEGENV.aux1 = SEGMENT.custom1/12; @@ -5609,8 +5596,8 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https } } */ - uint16_t _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); - byte _speed = map(SEGMENT.speed, 0, 255, 128, 16); + unsigned _scale = map(SEGMENT.intensity, 0, 255, 30, adjScale); + int _speed = map(SEGMENT.speed, 0, 255, 128, 16); for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { @@ -5633,13 +5620,13 @@ static const char _data_FX_MODE_2DPOLARLIGHTS[] PROGMEM = "Polar Lights@!,Scale; uint16_t mode_2DPulser(void) { // By: ldirko https://editor.soulmatelights.com/gallery/878-pulse-test , modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); SEGMENT.fadeToBlackBy(8 - (SEGMENT.intensity>>5)); uint32_t a = strip.now / (18 - SEGMENT.speed / 16); - uint16_t x = (a / 14) % cols; - uint16_t y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); + int x = (a / 14) % cols; + int y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); SEGMENT.blur(1 + (SEGMENT.intensity>>4)); @@ -5655,8 +5642,8 @@ static const char _data_FX_MODE_2DPULSER[] PROGMEM = "Pulser@!,Blur;;!;2"; uint16_t mode_2DSindots(void) { // By: ldirko https://editor.soulmatelights.com/gallery/597-sin-dots , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5667,8 +5654,8 @@ uint16_t mode_2DSindots(void) { // By: ldirko http byte t1 = strip.now / (257 - SEGMENT.speed); // 20; byte t2 = sin8(t1) / 4 * 2; for (int i = 0; i < 13; i++) { - byte x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! - byte y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! + int x = sin8(t1 + i * SEGMENT.intensity/8)*(cols-1)/255; // max index now 255x15/255=15! + int y = sin8(t2 + i * SEGMENT.intensity/8)*(rows-1)/255; // max index now 255x15/255=15! SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, i * 255 / 13, 255, LINEARBLEND)); } SEGMENT.blur(SEGMENT.custom2>>3); @@ -5686,22 +5673,21 @@ uint16_t mode_2Dsquaredswirl(void) { // By: Mark Kriegsman. https://g // Modifed by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint8_t kBorderWidth = 2; SEGMENT.fadeToBlackBy(24); - uint8_t blurAmount = SEGMENT.custom3>>1; // reduced resolution slider - SEGMENT.blur(blurAmount); + SEGMENT.blur(SEGMENT.custom3>>1); // Use two out-of-sync sine waves - uint8_t i = beatsin8(19, kBorderWidth, cols-kBorderWidth); - uint8_t j = beatsin8(22, kBorderWidth, cols-kBorderWidth); - uint8_t k = beatsin8(17, kBorderWidth, cols-kBorderWidth); - uint8_t m = beatsin8(18, kBorderWidth, rows-kBorderWidth); - uint8_t n = beatsin8(15, kBorderWidth, rows-kBorderWidth); - uint8_t p = beatsin8(20, kBorderWidth, rows-kBorderWidth); + int i = beatsin8(19, kBorderWidth, cols-kBorderWidth); + int j = beatsin8(22, kBorderWidth, cols-kBorderWidth); + int k = beatsin8(17, kBorderWidth, cols-kBorderWidth); + int m = beatsin8(18, kBorderWidth, rows-kBorderWidth); + int n = beatsin8(15, kBorderWidth, rows-kBorderWidth); + int p = beatsin8(20, kBorderWidth, rows-kBorderWidth); SEGMENT.addPixelColorXY(i, m, ColorFromPalette(SEGPALETTE, strip.now/29, 255, LINEARBLEND)); SEGMENT.addPixelColorXY(j, n, ColorFromPalette(SEGPALETTE, strip.now/41, 255, LINEARBLEND)); @@ -5718,8 +5704,8 @@ static const char _data_FX_MODE_2DSQUAREDSWIRL[] PROGMEM = "Squared Swirl@,,,,Bl uint16_t mode_2DSunradiation(void) { // By: ldirko https://editor.soulmatelights.com/gallery/599-sun-radiation , modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (!SEGENV.allocateData(sizeof(byte)*(cols+2)*(rows+2))) return mode_static(); //allocation failed byte *bump = reinterpret_cast(SEGENV.data); @@ -5729,7 +5715,7 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi } unsigned long t = strip.now / 4; - int index = 0; + unsigned index = 0; uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { for (int i = 0; i < (cols + 2); i++) { @@ -5739,16 +5725,16 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi } int yindex = cols + 3; - int16_t vly = -(rows / 2 + 1); + int vly = -(rows / 2 + 1); for (int y = 0; y < rows; y++) { ++vly; - int16_t vlx = -(cols / 2 + 1); + int vlx = -(cols / 2 + 1); for (int x = 0; x < cols; x++) { ++vlx; - int8_t nx = bump[x + yindex + 1] - bump[x + yindex - 1]; - int8_t ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; - byte difx = abs8(vlx * 7 - nx); - byte dify = abs8(vly * 7 - ny); + int nx = bump[x + yindex + 1] - bump[x + yindex - 1]; + int ny = bump[x + yindex + (cols + 2)] - bump[x + yindex - (cols + 2)]; + unsigned difx = abs8(vlx * 7 - nx); + unsigned dify = abs8(vly * 7 - ny); int temp = difx * difx + dify * dify; int col = 255 - temp / 8; //8 its a size of effect if (col < 0) col = 0; @@ -5768,8 +5754,8 @@ static const char _data_FX_MODE_2DSUNRADIATION[] PROGMEM = "Sun Radiation@Varian uint16_t mode_2Dtartan(void) { // By: Elliott Kember https://editor.soulmatelights.com/gallery/3-tartan , Modified by: Andrew Tuline if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -5807,12 +5793,12 @@ static const char _data_FX_MODE_2DTARTAN[] PROGMEM = "Tartan@X scale,Y scale,,,S uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [https://editor.soulmatelights.com/gallery/639-space-ships], adapted by Blaz Kristan (AKA blazoncek) if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint32_t tb = strip.now >> 12; // every ~4s if (tb > SEGENV.step) { - int8_t dir = ++SEGENV.aux0; + int dir = ++SEGENV.aux0; dir += (int)random8(3)-1; if (dir > 7) SEGENV.aux0 = 0; else if (dir < 0) SEGENV.aux0 = 7; @@ -5824,8 +5810,8 @@ uint16_t mode_2Dspaceships(void) { //// Space ships by stepko (c)05.02.21 [ht SEGMENT.move(SEGENV.aux0, 1); for (size_t i = 0; i < 8; i++) { - byte x = beatsin8(12 + i, 2, cols - 3); - byte y = beatsin8(15 + i, 2, rows - 3); + int x = beatsin8(12 + i, 2, cols - 3); + int y = beatsin8(15 + i, 2, rows - 3); CRGB color = ColorFromPalette(SEGPALETTE, beatsin8(12 + i, 0, 255), 255); SEGMENT.addPixelColorXY(x, y, color); if (cols > 24 || rows > 24) { @@ -5850,8 +5836,8 @@ static const char _data_FX_MODE_2DSPACESHIPS[] PROGMEM = "Spaceships@!,Blur;;!;2 uint16_t mode_2Dcrazybees(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); byte n = MIN(MAX_BEES, (rows * cols) / 256 + 1); @@ -5895,7 +5881,7 @@ uint16_t mode_2Dcrazybees(void) { SEGMENT.addPixelColorXY(bee[i].aimX, bee[i].aimY - 1, CHSV(bee[i].hue, 255, 255)); if (bee[i].posX != bee[i].aimX || bee[i].posY != bee[i].aimY) { SEGMENT.setPixelColorXY(bee[i].posX, bee[i].posY, CRGB(CHSV(bee[i].hue, 60, 255))); - int8_t error2 = bee[i].error * 2; + int error2 = bee[i].error * 2; if (error2 > -bee[i].deltaY) { bee[i].error -= bee[i].deltaY; bee[i].posX += bee[i].signX; @@ -5913,19 +5899,20 @@ uint16_t mode_2Dcrazybees(void) { return FRAMETIME; } static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur;;;2"; +#undef MAX_BEES - +#ifndef DISABLE_2D_PS_REPLACEMENTS ///////////////////////// // 2D Ghost Rider // ///////////////////////// //// Ghost Rider by stepko (c)2021 [https://editor.soulmatelights.com/gallery/716-ghost-rider], adapted by Blaz Kristan (AKA blazoncek) -/* + #define LIGHTERS_AM 64 // max lighters (adequate for 32x32 matrix) uint16_t mode_2Dghostrider(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); typedef struct Lighter { int16_t gPosX; @@ -6004,19 +5991,21 @@ uint16_t mode_2Dghostrider(void) { return FRAMETIME; } static const char _data_FX_MODE_2DGHOSTRIDER[] PROGMEM = "Ghost Rider@Fade rate,Blur;;!;2"; -*/ +#undef LIGHTERS_AM +#endif //DISABLE_2D_PS_REPLACEMENTS +#ifndef DISABLE_2D_PS_REPLACEMENTS //////////////////////////// // 2D Floating Blobs // //////////////////////////// //// Floating Blobs by stepko (c)2021 [https://editor.soulmatelights.com/gallery/573-blobs], adapted by Blaz Kristan (AKA blazoncek) -/* + #define MAX_BLOBS 8 uint16_t mode_2Dfloatingblobs(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); typedef struct Blob { float x[MAX_BLOBS], y[MAX_BLOBS]; @@ -6026,7 +6015,7 @@ uint16_t mode_2Dfloatingblobs(void) { byte color[MAX_BLOBS]; } blob_t; - uint8_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this + size_t Amount = (SEGMENT.intensity>>5) + 1; // NOTE: be sure to update MAX_BLOBS if you change this if (!SEGENV.allocateData(sizeof(blob_t))) return mode_static(); //allocation failed blob_t *blob = reinterpret_cast(SEGENV.data); @@ -6103,9 +6092,9 @@ uint16_t mode_2Dfloatingblobs(void) { return FRAMETIME; } -#undef MAX_BLOBS static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; -*/ +#undef MAX_BLOBS +#endif //DISABLE_2D_PS_REPLACEMENTS //////////////////////////// // 2D Scrolling text // @@ -6113,11 +6102,11 @@ static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail; uint16_t mode_2Dscrollingtext(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - int letterWidth, rotLW; - int letterHeight, rotLH; + unsigned letterWidth, rotLW; + unsigned letterHeight, rotLH; switch (map(SEGMENT.custom2, 0, 255, 1, 5)) { default: case 1: letterWidth = 4; letterHeight = 6; break; @@ -6214,8 +6203,8 @@ static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Off uint16_t mode_2Ddriftrose(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const float CX = (cols-cols%2)/2.f - .5f; const float CY = (rows-rows%2)/2.f - .5f; @@ -6241,15 +6230,15 @@ static const char _data_FX_MODE_2DDRIFTROSE[] PROGMEM = "Drift Rose@Fade,Blur;;; uint16_t mode_2Dplasmarotozoom() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t dataSize = SEGMENT.length() + sizeof(float); + unsigned dataSize = SEGMENT.length() + sizeof(float); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed float *a = reinterpret_cast(SEGENV.data); byte *plasma = reinterpret_cast(SEGENV.data+sizeof(float)); - uint16_t ms = strip.now/15; + unsigned ms = strip.now/15; // plasma for (int j = 0; j < rows; j++) { @@ -6270,7 +6259,7 @@ uint16_t mode_2Dplasmarotozoom() { for (int j = 0; j < rows; j++) { byte u = abs8(u1 - j * sinus) % cols; byte v = abs8(v1 + j * kosinus) % rows; - SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 0)); + SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255)); } } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed @@ -6334,8 +6323,8 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli // This currently has no controls. #define maxsteps 16 // Case statement wouldn't allow a variable. - uint16_t maxRipples = 16; - uint16_t dataSize = sizeof(Ripple) * maxRipples; + unsigned maxRipples = 16; + unsigned dataSize = sizeof(Ripple) * maxRipples; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); @@ -6414,8 +6403,8 @@ static const char _data_FX_MODE_RIPPLEPEAK[] PROGMEM = "Ripple Peak@Fade rate,Ma uint16_t mode_2DSwirl(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6425,18 +6414,18 @@ uint16_t mode_2DSwirl(void) { SEGMENT.blur(SEGMENT.custom1); - uint8_t i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); - uint8_t j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); - uint8_t ni = (cols - 1) - i; - uint8_t nj = (cols - 1) - j; + int i = beatsin8( 27*SEGMENT.speed/255, borderWidth, cols - borderWidth); + int j = beatsin8( 41*SEGMENT.speed/255, borderWidth, rows - borderWidth); + int ni = (cols - 1) - i; + int nj = (cols - 1) - j; um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio um_data = simulateSound(SEGMENT.soundSim); } - float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? - int16_t volumeRaw = *(int16_t*) um_data->u_data[1]; + float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? + int volumeRaw = *(int16_t*) um_data->u_data[1]; SEGMENT.addPixelColorXY( i, j, ColorFromPalette(SEGPALETTE, (strip.now / 11 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 11, 200, 255); SEGMENT.addPixelColorXY( j, i, ColorFromPalette(SEGPALETTE, (strip.now / 13 + volumeSmth*4), volumeRaw * SEGMENT.intensity / 64, LINEARBLEND)); //CHSV( ms / 13, 200, 255); @@ -6457,8 +6446,8 @@ static const char _data_FX_MODE_2DSWIRL[] PROGMEM = "Swirl@!,Sensitivity,Blur;,B uint16_t mode_2DWaverly(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -6471,20 +6460,20 @@ uint16_t mode_2DWaverly(void) { long t = strip.now / 2; for (int i = 0; i < cols; i++) { - uint16_t thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; // use audio if available if (um_data) { thisVal /= 32; // reduce intensity of inoise8() thisVal *= volumeSmth; } - uint16_t thisMax = map(thisVal, 0, 512, 0, rows); + int thisMax = map(thisVal, 0, 512, 0, rows); for (int j = 0; j < thisMax; j++) { SEGMENT.addPixelColorXY(i, j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); } } - SEGMENT.blur(16); + SEGMENT.blur(cols*rows > 100 ? 16 : 0); return FRAMETIME; } // mode_2DWaverly() @@ -6509,7 +6498,7 @@ typedef struct Gravity { uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); - const uint16_t dataSize = sizeof(gravity); + const unsigned dataSize = sizeof(gravity); if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); @@ -6527,7 +6516,7 @@ uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivity" upscaling float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0f); // map to pixels available in current segment - uint16_t tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. + int tempsamp = constrain(mySampleAvg, 0, SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; i(SEGENV.data); @@ -6663,7 +6652,7 @@ uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% - uint16_t my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); + unsigned my_sampleAgc = fmax(fmin(volumeSmth, 255.0), 0); for (size_t i=0; iu_data[1]; + int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -6763,7 +6752,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. if (SEGENV.call == 0) SEGMENT.fill(BLACK); for (int i = 0; i < SEGLEN; i++) { - uint16_t index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. + unsigned index = inoise8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. @@ -6787,7 +6776,7 @@ uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. um_data = simulateSound(SEGMENT.soundSim); } float volumeSmth = *(float*) um_data->u_data[0]; - int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + int volumeRaw = *(int16_t*)um_data->u_data[1]; //uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); uint8_t fadeRate = map(SEGMENT.speed,0,255,200,254); @@ -6827,7 +6816,7 @@ uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. // add support for no audio um_data = simulateSound(SEGMENT.soundSim); } - int16_t volumeRaw = *(int16_t*)um_data->u_data[1]; + int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; if (SEGENV.aux0 != secondHand) { @@ -6893,9 +6882,9 @@ static const char _data_FX_MODE_PLASMOID[] PROGMEM = "Plasmoid@Phase,# of pixels uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); - uint16_t size = 0; + unsigned size = 0; uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); - uint16_t pos = random16(SEGLEN); // Set a random starting position. + unsigned pos = random16(SEGLEN); // Set a random starting position. um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { @@ -6922,7 +6911,7 @@ uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. if (pos+size>= SEGLEN) size = SEGLEN - pos; } - for (int i=0; i(SEGENV.data); @@ -7338,7 +7324,7 @@ uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuli uint8_t numBins = map(SEGMENT.intensity,0,255,0,16); // Map slider to fftResult bins. for (int i=0; i(SEGENV.data); //array of previous bar heights per frequency band @@ -7477,8 +7462,8 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. uint8_t band = map(x, 0, cols-1, 0, NUM_BANDS - 1); if (NUM_BANDS < 16) band = map(band, 0, NUM_BANDS - 1, 0, 15); // always use full range. comment out this line to get the previous behaviour. band = constrain(band, 0, 15); - uint16_t colorIndex = band * 17; - uint16_t barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here + unsigned colorIndex = band * 17; + int barHeight = map(fftResult[band], 0, 255, 0, rows); // do not subtract -1 from rows here if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up uint32_t ledColor = BLACK; @@ -7506,8 +7491,8 @@ static const char _data_FX_MODE_2DGEQ[] PROGMEM = "GEQ@Fade speed,Ripple decay,# uint16_t mode_2DFunkyPlank(void) { // Written by ??? Adapted by Will Tatam. if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); int NUMB_BANDS = map(SEGMENT.custom1, 0, 255, 1, 16); int barWidth = (cols / NUMB_BANDS); @@ -7598,10 +7583,10 @@ static uint8_t akemi[] PROGMEM = { uint16_t mode_2DAkemi(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); - uint16_t counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; + unsigned counter = (strip.now * ((SEGMENT.speed >> 2) +2)) & 0xFFFF; counter = counter >> 8; const float lightFactor = 0.15f; @@ -7617,9 +7602,9 @@ uint16_t mode_2DAkemi(void) { //draw and color Akemi for (int y=0; y < rows; y++) for (int x=0; x < cols; x++) { CRGB color; - CRGB soundColor = ORANGE; - CRGB faceColor = SEGMENT.color_wheel(counter); - CRGB armsAndLegsColor = SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0; //default warmish white 0xABA8FF; //0xFF52e5;// + CRGB soundColor = CRGB::Orange; + CRGB faceColor = CRGB(SEGMENT.color_wheel(counter)); + CRGB armsAndLegsColor = CRGB(SEGCOLOR(1) > 0 ? SEGCOLOR(1) : 0xFFE0A0); //default warmish white 0xABA8FF; //0xFF52e5;// uint8_t ak = pgm_read_byte_near(akemi + ((y * 32)/rows) * 32 + (x * 32)/cols); // akemi[(y * 32)/rows][(x * 32)/cols] switch (ak) { case 3: armsAndLegsColor.r *= lightFactor; armsAndLegsColor.g *= lightFactor; armsAndLegsColor.b *= lightFactor; color = armsAndLegsColor; break; //light arms and legs 0x9B9B9B @@ -7643,10 +7628,10 @@ uint16_t mode_2DAkemi(void) { //add geq left and right if (um_data && fftResult) { for (int x=0; x < cols/8; x++) { - uint16_t band = x * cols/8; + unsigned band = x * cols/8; band = constrain(band, 0, 15); - uint16_t barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); - CRGB color = SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0); + int barHeight = map(fftResult[band], 0, 255, 0, 17*rows/32); + CRGB color = CRGB(SEGMENT.color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0)); for (int y=0; y < barHeight; y++) { SEGMENT.setPixelColorXY(x, rows/2-y, color); @@ -7666,29 +7651,29 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea uint16_t mode_2Ddistortionwaves() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint8_t speed = SEGMENT.speed/32; uint8_t scale = SEGMENT.intensity/32; uint8_t w = 2; - uint16_t a = strip.now/32; - uint16_t a2 = a/2; - uint16_t a3 = a/3; + unsigned a = strip.now/32; + unsigned a2 = a/2; + unsigned a3 = a/3; - uint16_t cx = beatsin8(10-speed,0,cols-1)*scale; - uint16_t cy = beatsin8(12-speed,0,rows-1)*scale; - uint16_t cx1 = beatsin8(13-speed,0,cols-1)*scale; - uint16_t cy1 = beatsin8(15-speed,0,rows-1)*scale; - uint16_t cx2 = beatsin8(17-speed,0,cols-1)*scale; - uint16_t cy2 = beatsin8(14-speed,0,rows-1)*scale; + unsigned cx = beatsin8(10-speed,0,cols-1)*scale; + unsigned cy = beatsin8(12-speed,0,rows-1)*scale; + unsigned cx1 = beatsin8(13-speed,0,cols-1)*scale; + unsigned cy1 = beatsin8(15-speed,0,rows-1)*scale; + unsigned cx2 = beatsin8(17-speed,0,cols-1)*scale; + unsigned cy2 = beatsin8(14-speed,0,rows-1)*scale; - uint16_t xoffs = 0; + unsigned xoffs = 0; for (int x = 0; x < cols; x++) { xoffs += scale; - uint16_t yoffs = 0; + unsigned yoffs = 0; for (int y = 0; y < rows; y++) { yoffs += scale; @@ -7721,8 +7706,8 @@ static const char _data_FX_MODE_2DDISTORTIONWAVES[] PROGMEM = "Distortion Waves@ uint16_t mode_2Dsoap() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const size_t dataSize = SEGMENT.width() * SEGMENT.height() * sizeof(uint8_t); // prevent reallocation if mirrored or grouped if (!SEGENV.allocateData(dataSize + sizeof(uint32_t)*3)) return mode_static(); //allocation failed @@ -7769,8 +7754,8 @@ uint16_t mode_2Dsoap() { int zD; int zF; int amplitude; - int8_t shiftX = 0; //(SEGMENT.custom1 - 128) / 4; - int8_t shiftY = 0; //(SEGMENT.custom2 - 128) / 4; + int shiftX = 0; //(SEGMENT.custom1 - 128) / 4; + int shiftY = 0; //(SEGMENT.custom2 - 128) / 4; CRGB ledsbuff[MAX(cols,rows)]; amplitude = (cols >= 16) ? (cols-8)/8 : 1; @@ -7833,8 +7818,8 @@ static const char _data_FX_MODE_2DSOAP[] PROGMEM = "Soap@!,Smoothness;;!;2"; uint16_t mode_2Doctopus() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); const uint8_t mapp = 180 / MAX(cols,rows); typedef struct { @@ -7872,8 +7857,8 @@ uint16_t mode_2Doctopus() { byte angle = rMap[XY(x,y)].angle; byte radius = rMap[XY(x,y)].radius; //CRGB c = CHSV(SEGENV.step / 2 - radius, 255, sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step) + radius - SEGENV.step * 2 + angle * (SEGMENT.custom3/3+1))); - uint16_t intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); - intensity = map(intensity*intensity, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display + unsigned intensity = sin8(sin8((angle * 4 - radius) / 4 + SEGENV.step/2) + radius - SEGENV.step + angle * (SEGMENT.custom3/4+1)); + intensity = map((intensity*intensity) & 0xFFFF, 0, 65535, 0, 255); // add a bit of non-linearity for cleaner display CRGB c = ColorFromPalette(SEGPALETTE, SEGENV.step / 2 - radius, intensity); SEGMENT.setPixelColorXY(x, y, c); } @@ -7889,8 +7874,8 @@ static const char _data_FX_MODE_2DOCTOPUS[] PROGMEM = "Octopus@!,,Offset X,Offse uint16_t mode_2Dwavingcell() { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up - const uint16_t cols = SEGMENT.virtualWidth(); - const uint16_t rows = SEGMENT.virtualHeight(); + const int cols = SEGMENT.virtualWidth(); + const int rows = SEGMENT.virtualHeight(); uint32_t t = strip.now/(257-SEGMENT.speed); uint8_t aX = SEGMENT.custom1/16 + 9; @@ -7903,7 +7888,7 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,,Amplitude 1,Amplitude 2,Amplitude 3;;!;2"; -#ifndef WLED_DISABLE_PARTICLESYSTEM +#ifndef WLED_DISABLE_PARTICLESYSTEM2D /* * Particle System Vortex @@ -7921,7 +7906,7 @@ uint16_t mode_particlevortex(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) return mode_static(); // allocation failed //SEGMENT.aux0 = 0; // starting angle @@ -7931,7 +7916,7 @@ uint16_t mode_particlevortex(void) #else PartSys->setMotionBlur(100); #endif - uint8_t numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + uint8_t numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center @@ -7951,7 +7936,7 @@ uint16_t mode_particlevortex(void) } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - uint8_t spraycount = min(PartSys->numSources, (uint8_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + uint8_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 #ifdef ESP8266 for (i = 1; i < 4; i++) // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed { @@ -7989,7 +7974,7 @@ uint16_t mode_particlevortex(void) if (SEGMENT.custom2 > 0) // automatic direction change enabled { - uint16_t changeinterval = (270 - SEGMENT.custom2); + uint16_t changeinterval = 15 + 255 / SEGMENT.custom2; direction = SEGMENT.aux1 & 0x02; //set direction according to flag if (SEGMENT.check3) // random interval @@ -8066,11 +8051,11 @@ uint16_t mode_particlefireworks(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES, true)) // init with advanced particle properties return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); //out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setWallHardness(100); //ground bounce is fixed - numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (j = 0; j < numRockets; j++) { PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon @@ -8086,7 +8071,7 @@ uint16_t mode_particlefireworks(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numRockets = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); PartSys->setWrapX(SEGMENT.check1); PartSys->setBounceY(SEGMENT.check2); @@ -8162,10 +8147,10 @@ uint16_t mode_particlefireworks(void) else { /* - //TODO: this does not look good. adjust or remove completely + if( PartSys->sources[j].source.vy < 0) //explosion is ongoing { - if(i < (emitparticles>>2)) //set 1/4 of particles to larger size + if(i < (emitparticles>>2)) //set 1/4 of particles to larger size //TODO: this does not look good. adjust or remove completely PartSys->sources[j].size = 50+random16(140); else PartSys->sources[j].size = 0; @@ -8235,7 +8220,7 @@ uint16_t mode_particlevolcano(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - PSsettings volcanosettings; + PSsettings2D volcanosettings; volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled uint8_t numSprays; // note: so far only one tested but more is possible uint32_t i = 0; @@ -8243,13 +8228,13 @@ uint16_t mode_particlevolcano(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed PartSys->setBounceY(true); PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); @@ -8269,7 +8254,7 @@ uint16_t mode_particlevolcano(void) return mode_static(); // something went wrong, no data! } - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); // number of sprays + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8321,7 +8306,7 @@ uint16_t mode_particlefire(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) + if (!initParticleSystem2D(PartSys, 25, 4)) //maximum number of source (PS will determine the exact number based on segment size) and need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed; //allocation failed SEGMENT.aux0 = random16(); // aux0 is wind position (index) in the perlin noise numFlames = PartSys->numSources; @@ -8440,7 +8425,7 @@ uint16_t mode_particlepit(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity @@ -8476,7 +8461,7 @@ uint16_t mode_particlepit(void) { // emit particle at random position over the top of the matrix (random16 is not random enough) PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + random16(500); // if speed is higher, make them die sooner - PartSys->particles[i].x = random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].x = random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); PartSys->particles[i].y = (PartSys->maxY<<1); // particles appear somewhere above the matrix, maximum is double the height PartSys->particles[i].vx = (int16_t)random(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed @@ -8503,53 +8488,14 @@ uint16_t mode_particlepit(void) if (SEGMENT.speed < 50) // for low speeds, apply more friction frictioncoefficient = 50 - SEGMENT.speed; - //if (SEGMENT.call % (3 + (SEGMENT.custom2 >> 2)) == 0) - //if (SEGMENT.call % (3 + (SEGMENT.speed >> 2)) == 0) if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled PartSys->applyFriction(frictioncoefficient); PartSys->update(); // update and render -/* -//rotat image (just a test, non working yet) - float angle = PI/3; - // Calculate sine and cosine of the angle - float cosTheta = cos(angle); - float sinTheta = sin(angle); - - // Center of rotation - int centerX = cols / 2; - int centerY = rows / 2; - - // Iterate over each pixel in the output image - for (int y = 0; y < rows; y++) - { - for (int x = 0; x < cols; x++) - { - int relX = x - centerX; - int relY = y - centerY; - - // Apply rotation using axis symmetry - int origX = round(relX * cosTheta - relY * sinTheta) + centerX; - int origY = round(relX * sinTheta + relY * cosTheta) + centerY; - - // Check if original coordinates are within bounds - if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) - { - // Copy pixel value from original image to rotated image - SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); - } - - // Copy pixel values from original image to rotated image - rotatedImage[origY][origX] = image[y][x]; - rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; - rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; - rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; - } - }*/ return FRAMETIME; } -static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=200,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o1=0,o2=0,o3=1"; /* * Particle Waterfall @@ -8566,12 +8512,12 @@ uint16_t mode_particlewaterfall(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 12)) // init, request 12 sources, no additional data needed + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setGravity(); // enable with default gforce PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setMotionBlur(190); // anable motion blur - numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (uint32_t)2)); // number of sprays + numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel/5, (int32_t)2)); // number of sprays for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.hue = random16(); @@ -8599,7 +8545,7 @@ uint16_t mode_particlewaterfall(void) PartSys->setBounceX(SEGMENT.check2); // walls PartSys->setBounceY(SEGMENT.check3); // ground PartSys->setWallHardness(SEGMENT.custom2); - numSprays = min((uint32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (uint32_t)2)); // number of sprays depends on segment width + numSprays = min((int32_t)PartSys->numSources, min(PartSys->maxXpixel / 5, (int32_t)2)); // number of sprays depends on segment width if (SEGMENT.custom2 > 0) // collisions enabled PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness @@ -8653,15 +8599,15 @@ uint16_t mode_particlebox(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init + if (!initParticleSystem2D(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setBounceX(true); PartSys->setBounceY(true); // set max number of particles and save to aux1 for later #ifdef ESP8266 - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); + SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel) >> 1), PartSys->numParticles); #else - SEGMENT.aux1 = min((uint16_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles + SEGMENT.aux1 = min((uint32_t)((PartSys->maxXpixel * PartSys->maxYpixel)), PartSys->numParticles); // max number of particles #endif for (i = 0; i < SEGMENT.aux1; i++) { @@ -8734,7 +8680,7 @@ uint16_t mode_particlebox(void) PartSys->applyForce(xgravity, ygravity); } - if (SEGMENT.call % (32-SEGMENT.custom3) == 0) + if (SEGMENT.call % (32 - SEGMENT.custom3) == 0) PartSys->applyFriction(2); PartSys->update(); // update and render @@ -8756,7 +8702,7 @@ uint16_t mode_particleperlin(void) uint32_t i; if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, 0, true)) // init with 1 source and advanced properties + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles SEGMENT.aux0 = rand(); @@ -8826,17 +8772,17 @@ uint16_t mode_particleimpact(void) ParticleSystem *PartSys = NULL; uint32_t i = 0; uint8_t MaxNumMeteors; - PSsettings meteorsettings; + PSsettings2D meteorsettings; meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(false); // explosions out of frame ar allowed, set to true to save particles (TODO: better enable it in ESP8266?) PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce - MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < MaxNumMeteors; i++) { PartSys->sources[i].source.y = 500; @@ -8859,7 +8805,7 @@ uint16_t mode_particleimpact(void) PartSys->setBounceX(SEGMENT.check2); PartSys->setWallHardness(SEGMENT.custom2); PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom2); // enable collisions and set particle collision hardness - MaxNumMeteors = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); uint8_t numMeteors = map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation uint32_t emitparticles; // number of particles to emit for each rocket's state @@ -8960,13 +8906,12 @@ uint16_t mode_particleattractor(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - uint32_t i = 0; - PSsettings sourcesettings; + PSsettings2D sourcesettings; sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) PSparticle *attractor; // particle pointer to the attractor if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings return mode_static(); // allocation failed //DEBUG_PRINTF_P(PSTR("sources in FX %p\n"), &PartSys->sources[0]); PartSys->sources[0].source.hue = random16(); @@ -9046,8 +8991,15 @@ uint16_t mode_particleattractor(void) PartSys->pointAttractor(i, attractor, strength, false); } } + else //no data, do classic attractor + { + for(uint32_t i = 0; i < displayparticles; i++) + { + PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); + } + } #else - for(i = 0; i < displayparticles; i++) + for(uint32_t i = 0; i < displayparticles; i++) { PartSys->pointAttractor(i, attractor, SEGMENT.speed, SEGMENT.check3); } @@ -9087,7 +9039,7 @@ uint16_t mode_particleattractor(void) if (SEGMENT.call == 0) // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. { - if (!initParticleSystem(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) + if (!initParticleSystem2D(PartSys, 1, true)) // init, need one source. use advanced particles (with individual forces) return mode_static(); // allocation failed; //allocation failed PartSys->sources[0].source.hue = random16(); @@ -9199,7 +9151,7 @@ uint16_t mode_particlespray(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->setBounceY(true); @@ -9297,7 +9249,7 @@ uint16_t mode_particleGEQ(void) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init + if (!initParticleSystem2D(PartSys, 1)) // init return mode_static(); // allocation failed PartSys->setKillOutOfBounds(true); PartSys->setUsedParticles((PartSys->numParticles * 3) / 2); // use 2/3 of available particles @@ -9397,9 +9349,9 @@ if (SEGLEN == 1) if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources return mode_static(); // allocation failed - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); for (i = 0; i < numSprays; i++) { PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center @@ -9419,7 +9371,7 @@ if (SEGLEN == 1) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - numSprays = min(PartSys->numSources, (uint8_t)NUMBEROFSOURCES); + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) @@ -9461,7 +9413,6 @@ if (SEGLEN == 1) PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); j = (j + 1) % numSprays; } - PartSys->update(); // update and render return FRAMETIME; } @@ -9476,12 +9427,12 @@ uint16_t mode_particleghostrider(void) if (SEGLEN == 1) return mode_static(); ParticleSystem *PartSys = NULL; - PSsettings ghostsettings; + PSsettings2D ghostsettings; ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY if (SEGMENT.call == 0) // initialization { - if (!initParticleSystem(PartSys, 1)) // init, no additional data needed + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed return mode_static(); // allocation failed; //allocation failed PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) PartSys->sources[0].maxLife = 260; // lifetime in frames @@ -9522,7 +9473,7 @@ uint16_t mode_particleghostrider(void) // color by age (PS 'color by age' always starts with hue = 255, don't want that here) if(SEGMENT.check1) { - for(int i = 0; i < PartSys->usedParticles; i++) + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); } @@ -9573,12 +9524,12 @@ uint16_t mode_particleblobs(void) if (SEGMENT.call == 0) { - if (!initParticleSystem(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) return mode_static(); // allocation failed PartSys->setBounceX(true); PartSys->setBounceY(true); PartSys->setWallHardness(255); - PartSys->setWallRoughness(255); + PartSys->setWallRoughness(255); PartSys->setCollisionHardness(255); //PartSys->setParticleSize(0); //set global size to zero or motion blur cannot be used (is zero by default) } @@ -9591,15 +9542,15 @@ uint16_t mode_particleblobs(void) return mode_static(); // something went wrong, no data! } PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setUsedParticles(min(PartSys->numParticles, (uint16_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); + PartSys->setUsedParticles(min(PartSys->numParticles, (uint32_t)map(SEGMENT.intensity,0 ,255, 1, (PartSys->maxXpixel * PartSys->maxYpixel)>>4))); PartSys->enableParticleCollisions(SEGMENT.check2); for (uint32_t i = 0; i < PartSys->usedParticles; i++) // update particles { if(SEGMENT.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) // speed changed or dead { - PartSys->particles[i].vx = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); - PartSys->particles[i].vy = (int16_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vx = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); + PartSys->particles[i].vy = (int8_t)random(-(SEGMENT.speed >> 2), SEGMENT.speed >> 2); } if(SEGMENT.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + random((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size @@ -9640,55 +9591,1190 @@ uint16_t mode_particleblobs(void) } } #endif + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); PartSys->update(); // update and render + /* +//rotat image (just a test, non working yet) + float angle = PI/3; + // Calculate sine and cosine of the angle + float cosTheta = cos(angle); + float sinTheta = sin(angle); + + // Center of rotation + int centerX = cols / 2; + int centerY = rows / 2; + + // Iterate over each pixel in the output image + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < cols; x++) + { + int relX = x - centerX; + int relY = y - centerY; + + // Apply rotation using axis symmetry + int origX = round(relX * cosTheta - relY * sinTheta) + centerX; + int origY = round(relX * sinTheta + relY * cosTheta) + centerY; + + // Check if original coordinates are within bounds + if (origX >= 0 && origX < rows && origY >= 0 && origY < cols) + { + // Copy pixel value from original image to rotated image + SEGMENT.setPixelColorXY(x, y, SEGMENT.getPixelColorXY(origX, origY)); + } + + // Copy pixel values from original image to rotated image + rotatedImage[origY][origX] = image[y][x]; + rotatedImage[origY][cols - 1 - origX] = image[y][cols - 1 - x]; + rotatedImage[rows - 1 - origY][origX] = image[rows - 1 - y][x]; + rotatedImage[rows - 1 - origY][cols - 1 - origX] = image[rows - 1 - y][cols - 1 - x]; + } + }*/ + return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o1=0,o2=0,o3=1"; -#endif //WLED_DISABLE_PARTICLESYSTEM +#endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D -////////////////////////////////////////////////////////////////////////////////////////// -// mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +/* +Particle Drip replacement, also replaces Rain +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleDrip(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + //uint8_t numSprays; + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 4)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = random16(); + SEGMENT.aux1 = 0xFFFF; // invalidate } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - } else { - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! } -} -void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3>>1); // set gravity (8 is default strength) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if(SEGMENT.check2) //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + else + PartSys->enableParticleCollisions(false); + + PartSys->sources[0].source.collide = false; //drops do not collide + + if(SEGMENT.check1) //rain mode, emit at random position, short life (3-8 seconds at 50fps) + { + if(SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; + PartSys->sources[0].source.x = random(PartSys->maxX); //random emit position + } + else{ //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } + + if(SEGMENT.aux1 != SEGMENT.intensity) //slider changed + SEGMENT.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + SEGMENT.aux1 = SEGMENT.intensity; + + // every nth frame emit a particle + if (SEGMENT.call % SEGMENT.aux0 == 0) + { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGMENT.aux0 = interval + random(interval + 5); + // if(SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else + PartSys->sources[0].source.hue = random16(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sprayEmit(PartSys->sources[0]); } - // now replace all pre-allocated effects - // --- 1D non-audio effects --- - addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); - addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); - addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); - addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++)//check all particles + { + if(PartSys->particles[i].ttl && PartSys->particles[i].collide == false) // use collision flag to identify splash particles + { + if(SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) //splash enabled and reached bottom + { + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].source.collide = true; //splashes do collide if enabled + for(int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) + { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + + if(SEGMENT.check1) //rain mode, fade hue to max + { + if(PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } + //increase speed on high settings by calling the move function twice + if(SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur/Overlay,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21,o1=0,o2=0,o3=0"; + + +/* +Particle Replacement for "Bbouncing Balls by Aircoookie" +Also replaces rolling balls and juggle (and maybe popcorn) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleBouncingBalls(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed; //allocation failed + PartSys->sources[0].source.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->sources[0].maxLife = 900; // maximum lifetime in frames + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + PartSys->setBounce(true); + SEGMENT.aux0 = 1; + SEGMENT.aux1 = 500; //set out of speed range to ensure uptate on first call + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWallHardness(hardness); + PartSys->setGravity(1 + (SEGMENT.custom3 >> 1)); // set gravity (8 is default strength) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->sources[0].var = SEGMENT.speed >> 3; + PartSys->sources[0].v = (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->enableParticleCollisions(SEGMENT.check1, hardness - 1); // enable collisions and set particle collision hardness (do not use full hardness or particles speed up due to pushing, can not be made perfectly balanced) + PartSys->setUsedParticles( 1 + (SEGMENT.intensity >> 3)); // 1 - 32 + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if(SEGMENT.check2) //rolling balls + { + PartSys->setGravity(0); + bool updatespeed = false; + if(SEGMENT.aux1 != SEGMENT.speed) + { + SEGMENT.aux1 = SEGMENT.speed; + updatespeed = true; + } + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].vx > 8 || PartSys->particles[i].vx < -8) //let only slow particles die (ensures no stopped particles) + PartSys->particles[i].ttl = 260; //set alive at full intensity + if(updatespeed || PartSys->particles[i].ttl == 0) //speed changed or particle died, reset TTL and speed + { + PartSys->particles[i].ttl = 260 + SEGMENT.speed; + PartSys->particles[i].collide = true; + int32_t newspeed = random(20 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + PartSys->particles[i].hue = random16(); //set ball colors to random + } + } + } + else //bouncing balls / popcorn + { + //check for balls that are 'laying on the ground' and remove them + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) + PartSys->particles[i].ttl = 0; + } + + // every nth frame emit a ball + if (SEGMENT.call % SEGMENT.aux0 == 0) + { + SEGMENT.aux0 = (260 - SEGMENT.intensity) + random(280 - SEGMENT.intensity); + PartSys->sources[0].source.hue = random16(); //set ball color + PartSys->sprayEmit(PartSys->sources[0]); + } + } + + //increase speed on high settings by calling the move function twice + if(SEGMENT.speed > 200) + { + for (uint32_t i = 0; i < PartSys->usedParticles; i++)//move all particles + { + PartSys->particleMoveUpdate(PartSys->particles[i]); + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PSBOUNCINGBALLS[] PROGMEM = "PS Bouncing Balls@Speed,!,Hardness,Blur/Overlay,Gravity,Collide,Rolling,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=240,c2=0,c3=8,o1=0,o2=0,o3=1"; + +/* +Particle Replacement for original Dancing Shadows: +"Spotlights moving back and forth that cast dancing shadows. +Shine this through tree branches/leaves or other close-up objects that cast +interesting shadows onto a ceiling or tarp. +By Steve Pomeroy @xxv" +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleDancingShadows(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_static(); // allocation failed; //allocation failed + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + //generate a spotlight: generates particles just outside of view + //if (SEGMENT.call % ((255 + 64) / (1 + SEGMENT.intensity + (SEGMENT.speed >> 4))) == 0) + if (SEGMENT.call % (256 - SEGMENT.intensity) == 0) + { + //random color, random type + uint32_t type = random8(SPOT_TYPES_COUNT); + int8_t speed = 2 + random(2 + (SEGMENT.speed >> 2)) + (SEGMENT.speed >> 3); + uint32_t width = random8(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (random8(2)) { + position = PartSys->maxXpixel; + speed = -speed; + }else { + position = -width; + } + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = random8(); //random spotlight color + for (uint32_t i = 0; i < width; i++) + { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if(i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if(i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if(i > 0) position += 3; //skip three pixels + i+=3; + break; + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; + position++; //do the next pixel + } + } + + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].outofbounds) //check if out of bounds particle move away from strip (i.e. vx < 0 && x > 0 or vx > 0 and x < 0) + { + if((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particles[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if(SEGMENT.aux0 != SEGMENT.speed) //speed changed + { + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + } + SEGMENT.aux0 = SEGMENT.speed; + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur/Overlay,Color Cycle,,,,Smooth;,!;!;1;pal=0,sx=100,ix=180,c1=0,c2=0,o2=0,o3=0"; + + +/* +Particle Fireworks 1D replacement +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleFireworks1D(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + //uint8_t numRockets; + uint8_t *forcecounter; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 4, 4, true)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + //numRockets = PartSys->numSources; + //for(i = 0; i < numRockets; i++) + //{ + PartSys->sources[0].source.perpetual = 1; //set rocket state to standby + //} + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + if(!SEGMENT.check1) //gravity enabled for sparks + PartSys->setGravity(0); // disable + else + PartSys->setGravity(1 + (SEGMENT.speed>>4)); // set gravity + + if(PartSys->sources[0].source.perpetual == 1) //rocket is on standby + { + PartSys->sources[0].source.ttl--; + if(PartSys->sources[0].source.ttl == 0) //time is up, relaunch + { + if(random(255) < SEGMENT.custom1) //randomly choose direction according to slider, fire at start of segment if true + SEGMENT.aux0 = 0; + else + SEGMENT.aux0 = 1; //invert direction + + PartSys->sources[0].source.perpetual = 0; //flag abused for rocket state + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = 0; + PartSys->sources[0].minLife = 10; + PartSys->sources[0].maxLife = 30; + PartSys->sources[0].source.x = 0; // start from bottom + uint32_t speed = 8 + (((int)16 + (int)random16(20) + (int)random(SEGMENT.speed >> 3 , SEGMENT.speed >> 2)) * (int)PartSys->maxXpixel) / 150; //set speed such that rocket explods in frame, found by experimenting + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 400; + PartSys->sources[0].source.collide = false; // exhaust does not collide, also used to check if direction reversed + PartSys->sources[0].sat = 40; // low saturation exhaust + PartSys->sources[0].size = 0; // default size + + if(SEGMENT.aux0) //inverted rockets launch from end + { + PartSys->sources[0].source.reversegrav = true; + PartSys->sources[0].source.x = PartSys->maxX; //start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; //revert direction + } + } + } + else //rocket is launched + { + int32_t rocketgravity = -(2 + (SEGMENT.speed>>4)); //-8 + int32_t speed = PartSys->sources[0].source.vx; + if(SEGMENT.aux0) //negative speed rocket + { + rocketgravity = -rocketgravity; + speed = -speed; + } + PartSys->applyForce(&PartSys->sources[0].source, rocketgravity, &forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source); + + if(speed < 0 && PartSys->sources[0].source.collide == false) //speed has reversed and not in 'explosion mode' + { + PartSys->sources[0].source.ttl = 75 - (SEGMENT.speed >> 2); //alive for a few more frames + PartSys->sources[0].source.collide = true; //set 'explosion mode' + } + + if(PartSys->sources[0].source.ttl == 0) //explode + { + PartSys->sources[0].source.perpetual = 1; // set standby state + PartSys->sources[0].var = 10 + (SEGMENT.intensity >> 2); + PartSys->sources[0].v = 0; //TODO can make global if this never changes + PartSys->sources[0].minLife = 60; + PartSys->sources[0].maxLife = 150; + PartSys->sources[0].source.ttl = 100 + random16(256 - SEGMENT.intensity); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation + PartSys->sources[0].size = random16(255); // random particle size in explosion + uint32_t explosionsize = 10 + random(SEGMENT.intensity >> 2, SEGMENT.intensity); + for(uint32_t e = 0; e < explosionsize; e++) //emit explosion particles + { + if(SEGMENT.check2) + PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + PartSys->sources[0].source.x = -500; //set out of frame until relaunch + } + } + if(SEGMENT.call & 0x01) //every second frame + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur/Overlay,Saturation,Gravity,Colorful,Smooth;,!;!;1;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21,o1=0,o2=1,o3=0"; + + +/* +Particle based Sparkle effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleSparkler(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t numSparklers; + uint32_t i; + PSsettings1D sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + sparklersettings.wrapX = SEGMENT.check2; + sparklersettings.bounceX = !SEGMENT.check2; + + numSparklers = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + for(i = 0; i < numSparklers; i++) + { + PartSys->sources[i].source.hue = random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.intensity >> 4 ; + PartSys->sources[i].minLife = 150 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 200 + SEGMENT.intensity; + uint32_t speed = SEGMENT.speed >> 1; + if(SEGMENT.check1) //invert spray speed + speed = -speed; + PartSys->sources[i].source.vx = speed; //update speed, do not change direction + PartSys->sources[i].source.ttl = 400; //replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; //color saturation + PartSys->particleMoveUpdate(PartSys->sources[i].source, &sparklersettings); //move sparkler + } + + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 2), (int)numSparklers); // set used sparklers, 1 to 8 + + if(SEGMENT.aux0 != SEGMENT.custom3) //number of used sparklers changed, redistribute + { + for(i = 1; i < numSparklers; i++) + { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGMENT.aux0 = SEGMENT.custom3; + + + for(i = 0; i < numSparklers; i++) + { + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Speed,!,Saturation,Blur/Overlay,Sparklers,Direction,Wrap/Bounce,Smooth;,!;!;1;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; + + +/* +Particle based Hourglass, particles falling at defined intervals +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleHourglass(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + int32_t positionoffset; // resting position offset + bool* direction; + uint8_t* basehue; + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 0, 2)) // init + return mode_static(); // allocation failed + PartSys->setBounce(true); + PartSys->setWallHardness(80); + + for(uint32_t i = 0; i < PartSys->numParticles; i++) + { + PartSys->particles[i].collide = true; + PartSys->particles[i].ttl = 500; + PartSys->particles[i].perpetual = true; + } + //SEGMENT.aux0 = 0; + SEGMENT.step = 0xFFFF; + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + basehue = PartSys->PSdataEnd; //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 1); //assign data pointer + uint32_t numgrains = map(SEGMENT.intensity, 0, 255, 1, PartSys->maxXpixel + 1); // number of particles to use + PartSys->setUsedParticles(min(numgrains, (uint32_t)PartSys->numParticles));//SEGMENT.custom1); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 34); // fixed hardness, 34 is a value that works best in most settings (spent a long time optimizing) SEGMENT.custom1); + + positionoffset = PS_P_RADIUS_1D / 2; + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 + + + if(SEGMENT.intensity != SEGMENT.step) //initialize + { + *basehue = random16(); //choose new random color + SEGMENT.step = SEGMENT.intensity; + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].reversegrav = true; + *direction = 0; + SEGMENT.aux1 = 1; //initialize below + } + SEGMENT.aux0 = PartSys->usedParticles - 1; //initial state, start with highest number particle + } + + for(uint32_t i = 0; i < PartSys->usedParticles; i++) //check if particle reached target position after falling + { + int32_t targetposition; + if (PartSys->particles[i].fixed == false) + { + //calculate target position depending on direction + if(PartSys->particles[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position + if(PartSys->particles[i].x == targetposition) //particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + PartSys->particles[i].fixed = true; + } + if(colormode == 7) + PartSys->setColorByPosition(true); //color fixed by position + else + { + PartSys->setColorByPosition(false); + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; //fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = *basehue; break; //fixed random color + case 2: + case 3: PartSys->particles[i].hue = *basehue + (i % colormode)*70; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = *basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = *basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 1); break; // disco! fast moving color gradient + default: break; + } + } + if(SEGMENT.check1 && !PartSys->particles[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + + if(SEGMENT.aux1 == 1) //last countdown call before dropping starts, reset all particles + { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) + { + uint32_t targetposition; + //calculate target position depending on direction + if(PartSys->particles[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionoffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionoffset; // target resting position -5 - PS_P_RADIUS_1D/2 + + PartSys->particles[i].x = targetposition; + PartSys->particles[i].fixed = true; + } + } + + if(SEGMENT.aux1 == 0) //countdown passed, run + { + uint32_t interval = 257 - SEGMENT.speed; // drop interval in frames, 1 second is 'speed = (257 - FPS)' speed = 0 is one drop every 257 frames + if(SEGMENT.check3 && *direction) // fast reset + interval = 3; + if(SEGMENT.call % interval == 0) //drop a particle, do not drop more often than every second frame or particles tangle up quite badly + { + if(SEGMENT.aux0 < PartSys->usedParticles) + { + PartSys->particles[SEGMENT.aux0].reversegrav = *direction; //let this particle fall or rise + PartSys->particles[SEGMENT.aux0].fixed = false; // unpin + } + else //overflow, flip direction + { + *direction = !(*direction); + SEGMENT.aux1 = 300; //set countdown + } + if(*direction == 0) //down + SEGMENT.aux0--; + else + SEGMENT.aux0++; + } + } + else if(SEGMENT.check2) //auto reset + SEGMENT.aux1--; //countdown + + //if(SEGMENT.call % (SEGMENT.speed >> 5) == 0) //more friction on higher falling rate to keep particles behaved + //if(SEGMENT.call % 6 == 0) + //PartSys->applyFriction(1); //keeps particles calm and stops mass collisions being handled improperly due to chaos + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Speed,!,Color,Blur/Overlay,Gravity,Colorflip,Auto Reset,Fast Reset;,!;!;1;pal=34,sx=245,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + + + +/* +Particle based Spray effect (like a volcano, possible replacement for popcorn) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1Dspray(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = (int32_t)SEGMENT.custom3 - 15; //gravity setting, 0-14 is negative, 16 - 31 is positive + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[0].source.hue = random16(); //TODO: add colormodes like in hourglass? + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].source.reversegrav = false; + if(gravity < 0) + PartSys->sources[0].source.reversegrav = true; + + if(random(255) % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); //overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].reversegrav = PartSys->sources[0].source.reversegrav; //update gravity direction + } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS 1D Spray@!,!,Position,Blur/Overlay,Gravity,Color by Age,Bounce,Color by Position;,!;!;1;pal=35,sx=200,ix=220,c1=4,c2=0,c3=28,o1=1,o2=1,o3=0"; + + +/* +Particle based balance: particles move back and forth (1D pendent to 2D particle box) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleBalance(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed + //PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + for(i = 0; i < PartSys->numParticles; i++) + { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].hue = (i * 1024) / PartSys->usedParticles; // multi gradient distribution + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; + PartSys->particles[i].collide = true; + } + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = map(SEGMENT.custom1, 0, 255, 50, 250); + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + if(SEGMENT.custom1 == 0) //collisions disabled, make the walls hard + hardness = 200; + PartSys->setWallHardness(hardness); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, PartSys->numParticles)); + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) // how often the force is applied depends on speed setting + { + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + SEGMENT.aux0 += increment; + if(SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGMENT.aux0) - 128); + else // sinusoidal + xgravity = (int16_t)cos8(SEGMENT.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGMENT.aux0) + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; + PartSys->applyForce(xgravity); + } + + uint32_t randomindex = random(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + + +//update colors + PartSys->setColorByPosition(SEGMENT.check1); + if(!SEGMENT.check1) + { + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].hue = (255 * i) / PartSys->usedParticles; //color by particle index + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Collisions,Blur/Overlay,Tilt,Color by Position,Wrap/Bounce,Random;,!;!;1;pal=18,sx=100,ix=40,c1=200,c2=0,c3=5,o1=1,o2=0,o3=0"; + + + +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleChase(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + PartSys->setWrap(true); + for(i = 0; i < PartSys->numParticles; i++) + { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].ttl = 300; + PartSys->particles[i].perpetual = true; + } + SEGMENT.aux0 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur(7 + (SEGMENT.custom3 << 3)); // anable motion blur + + //PartSys->setBounce(SEGMENT.check2); + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; + + if(SEGMENT.aux0 != settingssum) //settings changed changed, update + { + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, min(PartSys->maxX / (64 + (SEGMENT.custom1 >> 1)), (int32_t)(PartSys->numParticles)))); //depends on intensity and particle size (custom1) + for(i = 0; i < PartSys->usedParticles; i++) + { + PartSys->particles[i].x = i * (PartSys->maxX / (PartSys->usedParticles)); // distribute evenly + if(SEGMENT.custom2 == 0) + PartSys->particles[i].hue = (i * 256) / PartSys->usedParticles; // gradient distribution + else if(SEGMENT.custom2 == 255) + PartSys->particles[i].hue = random16(); + else + PartSys->particles[i].hue = SEGMENT.custom2; + int32_t speed = SEGMENT.speed >> 1; + if(SEGMENT.check1) speed = -speed; + PartSys->particles[i].vx = speed; + PartSys->advPartProps[i].size = SEGMENT.custom1; + } + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + SEGMENT.aux0 = settingssum; + } +PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@Speed,Density,Size,Color,Blur/Overlay,Direction,,Color by Position;,!;!;1;pal=53,sx=50,ix=100,c2=0,c3=0,o1=0,o2=0,o3=0"; + + +/* +Particle Fireworks Starburst replacement (smoother rendering, more settings) +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particleStarburst(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 1, 0, true)) // init + return mode_static(); // allocation failed + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if(PartSys->sources[0].source.ttl-- == 0) // stanby time elapsed TODO: make it a timer? + { + uint32_t explosionsize = 4 + random(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].source.collide = SEGMENT.check3; + for(uint32_t e = 0; e < explosionsize; e++) // emit particles + { + if(SEGMENT.check2) + PartSys->sources[0].source.hue = random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if(PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if(SEGMENT.call % 5 == 0) + { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Fragment Size,Blur/Overlay,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21,o1=0,o2=0,o3=0"; + + + +/* +Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ + +uint16_t mode_particle1DGEQ(void) +{ + if (SEGLEN == 1) + return mode_static(); + ParticleSystem1D *PartSys = NULL; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) // initialization + { + if (!initParticleSystem1D(PartSys, 16, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed + } + else + PartSys = reinterpret_cast(SEGMENT.data); // if not first call, just set the pointer to the PS + + if (PartSys == NULL) + { + DEBUG_PRINT(F("ERROR: FX PartSys nullpointer")); + return mode_static(); // something went wrong, no data! + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for(i = 0; i < numSources; i++) + { + PartSys->sources[i].source.hue = i * 16;//random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 3; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->setParticleSize(SEGMENT.custom1); + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for(i = 0; i < PartSys->usedParticles; i++) + { + if(PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) + um_data = simulateSound(SEGMENT.soundSim); // add support for no audio + + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin; //current bin + uint32_t threshold = 300 - SEGMENT.intensity; + + + for (bin = 0; bin < numSources; bin++) + { + uint32_t emitparticle = 0; + //uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) + { + emitparticle = 1; + } + else if(fftResult[bin] > 0)// band has low volue + { + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (random16() % restvolume == 0) + { + emitparticle = 1; + } + } + + if(emitparticle) + { + PartSys->sprayEmit(PartSys->sources[bin]); + } + } + //TODO: add color control? + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS 1D GEQ@Speed,!,Size,Blur/Overlay,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1,o3=0"; + + +#endif //WLED_DISABLE_PARTICLESYSTEM1D + +////////////////////////////////////////////////////////////////////////////////////////// +// mode data +static const char _data_RESERVED[] PROGMEM = "RSVD"; + +// add (or replace reserved) effect mode and data into vector +// use id==255 to find unallocated gaps (with "Reserved" data string) +// if vector size() is smaller than id (single) data is appended at the end (regardless of id) +void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { + if (id == 255) { // find empty slot + for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + } + if (id < _mode.size()) { + if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect + _mode[id] = mode_fn; + _modeData[id] = mode_name; + } else { + _mode.push_back(mode_fn); + _modeData.push_back(mode_name); + if (_modeCount < _mode.size()) _modeCount++; + } +} + +void WS2812FX::setupEffectData() { + // Solid must be first! (assuming vector is empty upon call to setup) + _mode.push_back(&mode_static); + _modeData.push_back(_data_FX_MODE_STATIC); + // fill reserved word in case there will be any gaps in the array + for (size_t i=1; i<_modeCount; i++) { + _mode.push_back(&mode_static); + _modeData.push_back(_data_RESERVED); + } + // now replace all pre-allocated effects + // --- 1D non-audio effects --- + addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); + addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); + addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); + addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); @@ -9725,14 +10811,16 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + #ifndef DISABLE_1D_PS_REPLACEMENTS addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + #endif + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); @@ -9751,7 +10839,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + #ifndef DISABLE_2D_PS_REPLACEMENTS addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + #endif addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); @@ -9774,14 +10864,17 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + #ifndef DISABLE_1D_PS_REPLACEMENTS addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + #endif + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); @@ -9796,8 +10889,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); @@ -9845,8 +10937,10 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); - //addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); - //addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #ifndef DISABLE_2D_PS_REPLACEMENTS + addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); + addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); @@ -9890,7 +10984,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio -#ifndef WLED_DISABLE_PARTICLESYSTEM +#ifndef WLED_DISABLE_PARTICLESYSTEM2D addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); @@ -9906,8 +11000,23 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECCIRCULARGEQ); -#endif // WLED_DISABLE_PARTICLESYSTEM +#endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSBOUNCINGBALLS, &mode_particleBouncingBalls, _data_FX_MODE_PSBOUNCINGBALLS); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); + + +#endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 4ed24153df..6e4eb9cbf9 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -332,7 +332,18 @@ #define FX_MODE_PARTICLECENTERGEQ 199 #define FX_MODE_PARTICLEGHOSTRIDER 200 #define FX_MODE_PARTICLEBLOBS 201 -#define MODE_COUNT 202 +#define FX_MODE_PSDRIP 202 +#define FX_MODE_PSBOUNCINGBALLS 203 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define FX_MODE_PSFIREWORKS1D 205 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define FX_MODE_PSCHASE 210 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define MODE_COUNT 213 typedef enum mapping1D2D { M12_Pixels = 0, @@ -876,7 +887,7 @@ class WS2812FX { // 96 bytes isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 64 + #define WLED_MAX_PANELS 18 uint8_t panels; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 96df692cf1..658b735498 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -149,10 +149,11 @@ Segment& Segment::operator= (Segment &&orig) noexcept { bool IRAM_ATTR Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); + DEBUG_PRINT(F("Allocating Data")); + // DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); deallocateData(); // if the old buffer was smaller release it first if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { // not enough memory @@ -1204,6 +1205,7 @@ void WS2812FX::finalizeInit(void) { // for the lack of better place enumerate ledmaps here // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads + // the other option is saving UI settings which will cause enumeration enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; @@ -1230,7 +1232,7 @@ void WS2812FX::finalizeInit(void) { unsigned start = prevLen; unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; - BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); + BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); if (BusManager::add(defCfg) == -1) break; } } @@ -1247,11 +1249,12 @@ void WS2812FX::finalizeInit(void) { unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; #ifdef ESP8266 - if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; - uint8_t pins[5]; - if (!bus->getPins(pins)) continue; - BusDigital* bd = static_cast(bus); - if (pins[0] == 3) bd->reinit(); + // why do we need to reinitialise GPIO3??? + //if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; + //uint8_t pins[5]; + //if (!bus->getPins(pins)) continue; + //BusDigital* bd = static_cast(bus); + //if (pins[0] == 3) bd->reinit(); #endif } @@ -1704,8 +1707,6 @@ void WS2812FX::printSize() { DEBUG_PRINTF_P(PSTR("Modes: %d*%d=%uB\n"), sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); DEBUG_PRINTF_P(PSTR("Data: %d*%d=%uB\n"), sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); DEBUG_PRINTF_P(PSTR("Map: %d*%d=%uB\n"), sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); - size = getLengthTotal(); - if (useGlobalLedBuffer) DEBUG_PRINTF_P(PSTR("Buffer: %d*%u=%uB\n"), sizeof(CRGB), size, size*sizeof(CRGB)); } #endif @@ -1769,13 +1770,15 @@ bool WS2812FX::deserializeMap(uint8_t n) { bool isFile = WLED_FS.exists(fileName); customMappingSize = 0; // prevent use of mapping if anything goes wrong + currentLedmap = 0; + if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { setUpMatrix(); return false; } - if (!isFile || !requestJSONBufferLock(7)) return false; // this will trigger setUpMatrix() when called from wled.cpp + if (!isFile || !requestJSONBufferLock(7)) return false; if (!readObjectFromFile(fileName, nullptr, pDoc)) { DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); @@ -1799,6 +1802,7 @@ bool WS2812FX::deserializeMap(uint8_t n) { if (!map.isNull() && map.size()) { // not an empty map customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal()); for (unsigned i=0; i= usedParticles) @@ -284,7 +287,7 @@ void ParticleSystem::angleEmit(PSsource &emitter, uint16_t angle, int8_t speed, // particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 // uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top -void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, PSadvancedParticle *advancedproperties) +void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings2D *options, PSadvancedParticle *advancedproperties) { if (options == NULL) options = &particlesettings; //use PS system settings by default @@ -316,8 +319,10 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) { if (options->wrapX) - { - newX = (uint16_t)newX % (maxX + 1); + { + newX = newX % (maxX + 1); + if (newX < 0) + newX += maxX + 1; } else if (((newX <= -PS_P_HALFRADIUS) || (newX > maxX + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -339,15 +344,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P if (options->bounceY) { - if ((newY < particleHardRadius) || (newY > maxY - particleHardRadius)) // reached floor / ceiling - { - if (newY < particleHardRadius) // bounce at bottom - bounce(part.vy, part.vx, newY, maxY); - else - { - if (!options->useGravity) - bounce(part.vy, part.vx, newY, maxY); - } + if ((newY < particleHardRadius) || ((newY > maxY - particleHardRadius) && !options->useGravity)) // reached floor / ceiling + { + bounce(part.vy, part.vx, newY, maxY); } } @@ -355,7 +354,9 @@ void ParticleSystem::particleMoveUpdate(PSparticle &part, PSsettings *options, P { if (options->wrapY) { - newY = (uint16_t)newY % (maxY + 1); + newY = newY % (maxY + 1); + if (newY < 0) + newY += maxY + 1; } else if (((newY <= -PS_P_HALFRADIUS) || (newY > maxY + PS_P_HALFRADIUS))) // particle is leaving, set out of bounds if it has fully left { @@ -391,7 +392,7 @@ void ParticleSystem::updateSize(PSadvancedParticle *advprops, PSsizeControl *adv // grow/shrink particle int32_t newsize = advprops->size; uint32_t counter = advsize->sizecounter; - uint32_t increment; + uint32_t increment = 0; // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds if (advsize->grow) increment = advsize->growspeed; else if (advsize->shrink) increment = advsize->shrinkspeed; @@ -475,13 +476,13 @@ void ParticleSystem::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_ position = maxposition - particleHardRadius; if (wallRoughness) { - int32_t totalspeed = abs(incomingspeed) + abs(parallelspeed); + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); // transfer an amount of incomingspeed speed to parallel speed - int32_t donatespeed = abs(incomingspeed); - donatespeed = (random(-donatespeed, donatespeed) * wallRoughness) / 255; //take random portion of + or - perpendicular speed, scaled by roughness + int32_t donatespeed = (random(-incomingspeed_abs, incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; //take random portion of + or - perpendicular speed, scaled by roughness parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); - incomingspeed = limitSpeed((int32_t)incomingspeed - donatespeed); - donatespeed = totalspeed - abs(parallelspeed); // keep total speed the same + //give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; } } @@ -577,16 +578,10 @@ void ParticleSystem::applyGravity() // function does not increment gravity counter, if gravity setting is disabled, this cannot be used void ParticleSystem::applyGravity(PSparticle *part) { - int32_t dv; // velocity increase - if (gforce > 15) - dv = (gforce >> 4); // apply the 4 MSBs - else - dv = 1; - - if (gforcecounter + gforce > 15) //counter is updated in global update when applying gravity - { - part->vy = limitSpeed((int32_t)part->vy - dv); - } + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, &gforcecounter); + gforcecounter = counterbkp; //save it back + part->vy = limitSpeed((int32_t)part->vy - dv); } // slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) @@ -728,7 +723,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) framebuffer = allocate2Dbuffer(maxXpixel + 1, maxYpixel + 1); if (framebuffer == NULL) { - Serial.println("Frame buffer alloc failed"); + //Serial.println("Frame buffer alloc failed"); useLocalBuffer = false; //render to segment pixels directly if not enough memory } else{ @@ -739,10 +734,10 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation { uint32_t yflipped; - for (uint32_t y = 0; y <= maxYpixel; y++) + for (int32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (uint32_t x = 0; x <= maxXpixel; x++) + for (int32_t x = 0; x <= maxXpixel; x++) { framebuffer[x][y] = SEGMENT.getPixelColorXY(x, yflipped); //copy to local buffer fast_color_scale(framebuffer[x][y], motionBlur); @@ -756,7 +751,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (!useLocalBuffer) //disabled or allocation above failed { - Serial.println("NOT using local buffer!"); + //Serial.println("NOT using local buffer!"); if (motionBlur > 0) SEGMENT.fadeToBlackBy(255 - motionBlur); else @@ -801,7 +796,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) uint32_t bluramount = particlesize; uint32_t bitshift = 0; - for(int i = 0; i < passes; i++) + for(uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; @@ -816,11 +811,11 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) if (useLocalBuffer) // transfer local buffer back to segment { - uint32_t yflipped; - for (int y = 0; y <= maxYpixel; y++) + int32_t yflipped; + for (int32_t y = 0; y <= maxYpixel; y++) { yflipped = maxYpixel - y; - for (int x = 0; x <= maxXpixel; x++) + for (int32_t x = 0; x <= maxXpixel; x++) { SEGMENT.setPixelColorXY(x, yflipped, framebuffer[x][y]); } @@ -832,7 +827,7 @@ void ParticleSystem::ParticleSys_render(bool firemode, uint32_t fireintensity) } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer) +void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer) { int32_t pxlbrightness[4] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs @@ -851,13 +846,13 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, { if (advPartProps[particleindex].size > 0) { - if (renderbuffer) + if (renderbuffer && framebuffer) { advancedrender = true; memset(renderbuffer[0], 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels } else - return; // cannot render without buffer, advanced size particles are allowed out of frame + return; // cannot render without buffers } } @@ -919,18 +914,18 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // calculate brightness values for all four pixels representing a particle using linear interpolation // precalculate values for speed optimization int32_t precal1 = (int32_t)PS_P_RADIUS - dx; - int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightess; - int32_t precal3 = dy * brightess; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; //calculate the values for pixels that are in frame if (pxlbrightness[0] >= 0) - pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE if (pxlbrightness[1] >= 0) - pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightess) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE if (pxlbrightness[2] >= 0) - pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightess) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE if (pxlbrightness[3] >= 0) - pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightess) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE if (advancedrender) { @@ -955,13 +950,13 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, } maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; - for(int i = 0; i < maxsize; i++) + for(uint32_t i = 0; i < maxsize; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; offset--; - blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); //blur to 4x4 + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, true, offset, offset, true); xsize = xsize > 64 ? xsize - 64 : 0; ysize = ysize > 64 ? ysize - 64 : 0; } @@ -969,7 +964,7 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, // calculate origin coordinates to render the particle to in the framebuffer uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; - uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) // transfer particle renderbuffer to framebuffer for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) @@ -978,11 +973,11 @@ void ParticleSystem::renderParticle(CRGB **framebuffer, uint32_t particleindex, if (xfb > maxXpixel) { if (particlesettings.wrapX) // wrap x to the other side if required - xfb = xfb % (maxXpixel + 1); + xfb = xfb % (maxXpixel + 1); //TODO: this did not work in 1D system but appears to work in 2D (wrapped pixels were offset) under which conditions does this not work? else continue; } - + for(uint32_t yrb = offset; yrb < rendersize+offset; yrb++) { yfb = yfb_orig + yrb; @@ -1126,7 +1121,7 @@ void ParticleSystem::handleCollisions() int32_t dx, dy; // distance to other particles for (j = i + 1; j < usedParticles; j++) // check against higher number particles { - if (particles[j].ttl > 0) // if target particle is alive + if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive { dx = particles[i].x - particles[j].x; if (advPartProps) //may be using individual particle size @@ -1185,7 +1180,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers // Calculate new velocities after collision - uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS ? PS_P_MINSURFACEHARDNESS : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift int32_t yimpulse = ((impulse) * dy) / 32767; @@ -1194,7 +1189,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx -= ximpulse; particle2->vy -= yimpulse; - if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely) + if (collisionHardness < surfacehardness) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) { const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); particle1->vx = ((int32_t)particle1->vx * coeff) / 255; @@ -1202,7 +1197,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx = ((int32_t)particle2->vx * coeff) / 255; particle2->vy = ((int32_t)particle2->vy * coeff) / 255; - +/* if (collisionHardness < 10) // if they are very soft, stop slow particles completely to make them stick to each other { particle1->vx = (particle1->vx < 3 && particle1->vx > -3) ? 0 : particle1->vx; @@ -1210,7 +1205,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl particle2->vx = (particle2->vx < 3 && particle2->vx > -3) ? 0 : particle2->vx; particle2->vy = (particle2->vy < 3 && particle2->vy > -3) ? 0 : particle2->vy; - } + }*/ } // particles have volume, push particles apart if they are too close @@ -1231,7 +1226,7 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl else particle1->x--; } - particle1->vx += push; + particle1->vx += push; //TODO: what happens if particle2 is also pushed? in 1D it stacks better, maybe also just reverse the comparison order so they flip roles? push = 0; if (dy < 0) push = pushamount; @@ -1246,42 +1241,20 @@ void ParticleSystem::collideParticles(PSparticle *particle1, PSparticle *particl } particle1->vy += push; // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 16) // if they are very soft, stop slow particles completely to make them stick to each other + { + particle1->vx = 0; + particle1->vy = 0; + particle2->vx = 0; + particle2->vy = 0; + //push them apart + particle1->x += push; + particle1->y += push; + } } } } -// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) -// force is in 3.4 fixedpoint notation, +/-127 -int32_t ParticleSystem::calcForce_dv(int8_t force, uint8_t* counter) -{ - if (force == 0) - return 0; - // for small forces, need to use a delay counter - int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) - int32_t dv; - // for small forces, need to use a delay counter, apply force only if it overflows - if (force_abs < 16) - { - *counter += force_abs; - if (*counter > 15) - { - *counter -= 16; - dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) - } - } - else - { - dv = force >> 4; // MSBs - } - return dv; -} - -// limit speed to prevent overflows -int32_t ParticleSystem::limitSpeed(int32_t speed) -{ - return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); -} - // allocate memory for the 2D array in one contiguous block and set values to zero CRGB **ParticleSystem::allocate2Dbuffer(uint32_t cols, uint32_t rows) { @@ -1347,8 +1320,69 @@ void ParticleSystem::updatePSpointers(bool isadvanced, bool sizecontrol) */ } +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) +{ + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + if (isparticle) //first and last row are always black in particle rendering + { + ystart++; + ysize--; + } + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + carryover = BLACK; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - xblur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1][y], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + } + + if (isparticle) // now also do first and last row + { + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) + { + carryover = BLACK; + for(uint32_t y = ystart; y < ystart + ysize; y++) + { + seeppart = colorbuffer[x][y]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x][y], 255 - yblur); + + if (y > 0) + { + fast_color_add(colorbuffer[x][y-1], seeppart); + fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster + } + carryover = seeppart; + } + fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel + } +} + + //non class functions to use for initialization -uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) +uint32_t calculateNumberOfParticles2D(bool isadvanced, bool sizecontrol) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1373,7 +1407,7 @@ uint32_t calculateNumberOfParticles(bool isadvanced, bool sizecontrol) return numberofParticles; } -uint32_t calculateNumberOfSources(uint8_t requestedsources) +uint32_t calculateNumberOfSources2D(uint8_t requestedsources) { uint32_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint32_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); @@ -1393,7 +1427,7 @@ uint32_t calculateNumberOfSources(uint8_t requestedsources) } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) +bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool isadvanced, bool sizecontrol, uint16_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem); // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1413,14 +1447,14 @@ bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bo } // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool largesizes, bool sizecontrol) +bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced, bool sizecontrol) { //Serial.println("PS init function"); - uint32_t numparticles = calculateNumberOfParticles(largesizes, sizecontrol); - uint32_t numsources = calculateNumberOfSources(requestedsources); + uint32_t numparticles = calculateNumberOfParticles2D(advanced, sizecontrol); + uint32_t numsources = calculateNumberOfSources2D(requestedsources); //Serial.print("numsources: "); //Serial.println(numsources); - if (!allocateParticleSystemMemory(numparticles, numsources, largesizes, sizecontrol, additionalbytes)) + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) { DEBUG_PRINT(F("PS init failed: memory depleted")); return false; @@ -1430,120 +1464,948 @@ bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //Serial.println("calling constructor"); - PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, largesizes, sizecontrol); // particle system constructor + PartSys = new (SEGMENT.data) ParticleSystem(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor //Serial.print("PS pointer at "); //Serial.println((uintptr_t)PartSys); return true; } -/////////////////////// -// Utility Functions // -/////////////////////// +#endif // WLED_DISABLE_PARTICLESYSTEM2D -// fastled color adding is very inaccurate in color preservation -// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow -// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) -// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) -{ - uint32_t r, g, b; - if (scale < 255) { - r = c1.r + ((c2.r * scale) >> 8); - g = c1.g + ((c2.g * scale) >> 8); - b = c1.b + ((c2.b * scale) >> 8); - } - else { - r = c1.r + c2.r; - g = c1.g + c2.g; - b = c1.b + c2.b; - } - uint32_t max = r; - if (g > max) // note: using ? operator would be slower by 2 instructions - max = g; - if (b > max) - max = b; - if (max < 256) - { - c1.r = r; // save result to c1 - c1.g = g; - c1.b = b; - } - else - { - c1.r = (r * 255) / max; - c1.g = (g * 255) / max; - c1.b = (b * 255) / max; - } -} -// faster than fastled color scaling as it uses a 32bit scale factor and pointer -void fast_color_scale(CRGB &c, uint32_t scale) +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + +ParticleSystem1D::ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced) { - c.r = ((c.r * scale) >> 8); - c.g = ((c.g * scale) >> 8); - c.b = ((c.b * scale) >> 8); + //Serial.println("PS Constructor"); + numSources = numberofsources; + numParticles = numberofparticles; // set number of particles in the array + usedParticles = numberofparticles; // use all particles by default + advPartProps = NULL; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = NULL; + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setSize(length); + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + setParticleSize(0); // minimum size by default + motionBlur = 0; //no fading by default + emitIndex = 0; + + //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) + { + sources[i].source.ttl = 1; //set source alive + } + //Serial.println("PS Constructor done"); } -// blur a matrix in x and y direction, blur can be asymmetric in x and y -// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined -// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) -void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear, uint32_t xstart, uint32_t ystart, bool isparticle) +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) { - CRGB seeppart, carryover; - uint32_t seep = xblur >> 1; - if (isparticle) //first and last row are always black in particle rendering - { - ystart++; - ysize--; - } - for(uint32_t y = ystart; y < ystart + ysize; y++) + PSadvancedParticle1D *advprop = NULL; + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works TODO: which one is really better for stacking / oscillations? + applyGravity(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { - carryover = BLACK; - for(uint32_t x = xstart; x < xstart + xsize; x++) + if (advPartProps) { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - xblur); - - if (x > 0) - { - fast_color_add(colorbuffer[x-1][y], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // TODO: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; + advprop = &advPartProps[i]; } - fast_color_add(colorbuffer[xsize-1][y], carryover); // set last pixel + particleMoveUpdate(particles[i], &particlesettings, advprop); } - if (isparticle) // now also do first and last row + if (particlesettings.colorByPosition) { - ystart--; - ysize++; + for (uint32_t i = 0; i < usedParticles; i++) + { + particles[i].hue = (255 * (uint32_t)particles[i].x) / maxX; + } } - seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) - { - carryover = BLACK; - for(uint32_t y = ystart; y < ystart + ysize; y++) - { - seeppart = colorbuffer[x][y]; // create copy of current color - fast_color_scale(seeppart, seep); // scale it and seep to neighbours - if (!smear) // fade current pixel if smear is disabled - fast_color_scale(colorbuffer[x][y], 255 - yblur); + ParticleSys_render(); - if (y > 0) - { - fast_color_add(colorbuffer[x][y-1], seeppart); - fast_color_add(colorbuffer[x][y], carryover); // todo: could check if carryover is > 0, takes 7 instructions, add takes ~35, with lots of same color pixels (like background), it would be faster - } - carryover = seeppart; + uint32_t bg_color = SEGCOLOR(1); //background color, set to black to overlay + if (bg_color > 0) //if not black + { + for(int32_t i = 0; i < maxXpixel + 1; i++) + { + SEGMENT.addPixelColor(i,bg_color); } - fast_color_add(colorbuffer[x][ysize-1], carryover); // set last pixel } } -#endif // WLED_DISABLE_PARTICLESYSTEM +void ParticleSystem1D::setUsedParticles(uint32_t num) +{ + usedParticles = min(num, numParticles); //limit to max particles +} + +void ParticleSystem1D::setWallHardness(uint8_t hardness) +{ + wallHardness = hardness; +} + +void ParticleSystem1D::setSize(uint16_t x) +{ + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(bool enable) +{ + particlesettings.wrapX = enable; +} + +void ParticleSystem1D::setBounce(bool enable) +{ + particlesettings.bounceX = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(bool enable) +{ + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(bool enable) +{ + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setColorByPosition(bool enable) +{ + particlesettings.colorByPosition = enable; +} + +void ParticleSystem1D::setMotionBlur(uint8_t bluramount) +{ + //TODO: currently normal blurring is not used in 1D system. should it be added? advanced rendering is quite fast and allows for motion blurring + // if (particlesize < 2) // only allwo motion blurring on default particle size or advanced size(cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +// render size using smearing (see blur function) +void ParticleSystem1D::setParticleSize(uint8_t size) +{ + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; // 1 pixel sized particles have half the radius (for bounce, not for collisions) + if (particlesize) + particleHardRadius = particleHardRadius << 1; // 2 pixel sized particles + //TODO: since global size rendering is always 1 or 2 pixels, this could maybe be made simpler with a bool 'singlepixelsize' +} +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem1D::setGravity(int8_t force) +{ + if (force) + { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::enableParticleCollisions(bool enable, uint8_t hardness) // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable +{ + particlesettings.useCollisions = enable; + collisionHardness = hardness; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem1D::sprayEmit(PSsource1D &emitter) +{ + for (int32_t i = 0; i < usedParticles; i++) + { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) // find a dead particle + { + particles[emitIndex].vx = emitter.v + random(-emitter.var, emitter.var); + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].collide = emitter.source.collide; + particles[emitIndex].reversegrav = emitter.source.reversegrav; + particles[emitIndex].ttl = random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } + return i; + + return emitIndex; + } + } + return -1; +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) +{ + if (options == NULL) + options = &particlesettings; //use PS system settings by default + if (part.ttl > 0) + { + if (!part.perpetual) + part.ttl--; // age + if (particlesettings.colorByAge) + part.hue = part.ttl > 250 ? 250 : part.ttl; //set color to ttl + + bool usesize = false; // particle uses individual size rendering + int32_t newX = part.x + (int16_t)part.vx; + part.outofbounds = 0; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + if (advancedproperties) //using individual particle size? + { + particleHardRadius = PS_P_MINHARDRADIUS_1D + advancedproperties->size; + if (advancedproperties->size > 1) + { + usesize = true; // note: variable eases out of frame checking below + } + else if (advancedproperties->size == 0) // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + } + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounceX) + { + if ((newX < particleHardRadius) || ((newX > maxX - particleHardRadius))) // reached a wall + { + bool bouncethis = true; + if (options->useGravity) + { + if (part.reversegrav) //skip at x = 0 + { + if (newX < particleHardRadius) + bouncethis = false; + } + else //skip at x = max + { + if (newX > particleHardRadius) + bouncethis = false; + } + } + + if (bouncethis) + { + part.vx = -part.vx; //invert speed + part.vx = ((int32_t)part.vx * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + } + } + if ((newX < 0) || (newX > maxX)) // check if particle reached an edge (note: this also checks out of bounds and must not be skipped, even if bounce is enabled) + { + if (options->wrapX) + { + newX = newX % (maxX + 1); + if (newX < 0) + newX += maxX + 1; + Serial.println(newX/32); + } + else if (((newX <= -PS_P_HALFRADIUS_1D) || (newX > maxX + PS_P_HALFRADIUS_1D))) // particle is leaving, set out of bounds if it has fully left + { + bool isleaving = true; + if (usesize) // using individual particle size + { + if (((newX > -particleHardRadius) && (newX < maxX + particleHardRadius))) // large particle is not yet leaving the view - note: this is not pixel perfect but good enough + isleaving = false; + } + if (isleaving) + { + part.outofbounds = 1; + if (options->killoutofbounds) + { + bool killthis = true; + if (options->useGravity) //if gravity is used, only kill below 'floor level' + { + if (part.reversegrav) //skip at x = 0 + { + if (newX < 0) + killthis = false; + } + else //skip at x = max + { + if (newX > 0) + killthis = false; + } + } + if (killthis) + part.ttl = 0; + } + } + } + } + if (!part.fixed) + part.x = (int16_t)newX; // set new position + else + part.vx = 0; //set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + } +} + +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter) +{ + // velocity increase + int32_t dv = calcForce_dv(xforce, counter); + // apply the force to particle + part->vx = limitSpeed((int32_t)part->vx + dv); +} + +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(int8_t xforce) +{ + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most compuatationally efficient way to do this, but it saves on duplacte code and is fast enough + for (uint i = 0; i < usedParticles; i++) + { + tempcounter = forcecounter; + applyForce(&particles[i], xforce, &tempcounter); + } + forcecounter = tempcounter; //save value back +} + +// apply gravity to all particles using PS global gforce setting +// gforce is in 3.4 fixed point notation, see note above +void ParticleSystem1D::applyGravity() +{ + int32_t dv_raw = calcForce_dv(gforce, &gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) + { + int32_t dv = dv_raw; + if (particles[i].reversegrav) dv = -dv_raw; + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D *part) +{ + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, &gforcecounter); + if (part->reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part->vx = limitSpeed((int32_t)part->vx - dv); +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) +{ + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) + { + if (particles[i].ttl) + particles[i].vx = ((int16_t)particles[i].vx * friction) / 255; + } +} + + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::ParticleSys_render() +{ + + CRGB baseRGB; + bool useLocalBuffer = true; //use local rendering buffer, gives huge speed boost (at least 30% more FPS) + CRGB *framebuffer = NULL; //local frame buffer + CRGB *renderbuffer = NULL; //local particle render buffer for advanced particles + uint32_t i; + uint32_t brightness; // particle brightness, fades if dying + + if (useLocalBuffer) + { + + // allocate empty memory for the local renderbuffer + framebuffer = allocate1Dbuffer(maxXpixel + 1); + if (framebuffer == NULL) + { + //Serial.println("Frame buffer alloc failed"); + useLocalBuffer = false; //render to segment pixels directly if not enough memory + } + else{ + if (advPartProps) + { + renderbuffer = allocate1Dbuffer(10); //buffer to render individual particles to if size > 0. note: null checking is done when accessing it + } + if (motionBlur > 0) // using SEGMENT.fadeToBlackBy is much slower, this approximately doubles the speed of fade calculation + { + for (uint32_t x = 0; x <= maxXpixel; x++) + { + framebuffer[x] = SEGMENT.getPixelColor(x); //copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); + } + } + } + } + + if (!useLocalBuffer) //disabled or allocation above failed + { + //Serial.println("NOT using local buffer!"); + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering to it + } + // go over particles and render them to the buffer + for (i = 0; i < usedParticles; i++) + { + if (particles[i].outofbounds || particles[i].ttl == 0) + continue; + + // generate RGB values for particle + brightness = particles[i].ttl > 255 ? 255 : particles[i].ttl; //faster then using min() + baseRGB = ColorFromPalette(SEGPALETTE, particles[i].hue, 255, LINEARBLEND); + + if (advPartProps) //saturation is advanced property in 1D system + { + if (advPartProps[i].sat < 255) + { + CHSV baseHSV = rgb2hsv_approximate(baseRGB); //convert to HSV + baseHSV.s = advPartProps[i].sat; //set the saturation + baseRGB = (CRGB)baseHSV; // convert back to RGB + } + } + renderParticle(framebuffer, i, brightness, baseRGB, renderbuffer); + } + + if (useLocalBuffer) // transfer local buffer back to segment + { + for (int x = 0; x <= maxXpixel; x++) + { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + free(framebuffer); + } + if (renderbuffer) + free(renderbuffer); +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem1D::renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer) +{ + uint32_t size = particlesize; + if (advPartProps) // use advanced size properties + { + size = advPartProps[particleindex].size; + } + if (size == 0) //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles + { + int32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + if (x <= maxXpixel) //by making x unsigned there is no need to check < 0 as it will overflow + { + if (framebuffer) + fast_color_add(framebuffer[x], color, brightness); + else + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness)); + } + } + else { //render larger particles + int32_t pxlbrightness[2] = {0}; // note: pxlbrightness needs to be set to 0 or checking does not work + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + // subtract half a radius as the rendering algorithm always starts at the left, this makes calculations more efficient + int32_t xoffset = particles[particleindex].x - PS_P_HALFRADIUS_1D; + int32_t dx = xoffset % PS_P_RADIUS_1D; //relativ particle position in subpixel space + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS which is 64, so can bitshift (compiler may not optimize automatically) + + // set the raw pixel coordinates + pixco[0] = x; // left pixel + pixco[1] = x + 1; // right pixel + + // now check if any are out of frame. set values to -1 if they are so they can be easily checked after (no value calculation, no setting of pixelcolor if value < 0) + if (x < 0) // left pixels out of frame + { + dx = PS_P_RADIUS_1D + dx; // if x<0, xoffset becomes negative (and so does dx), must adjust dx as modulo will flip its value + // note: due to inverted shift math, a particel at position -32 (xoffset = -64, dx = 64) is rendered at the wrong pixel position (it should be out of frame) + // checking this above makes this algorithm slower (in frame pixels do not have to be checked), so just correct for it here: + if (dx == PS_P_RADIUS_1D) + { + pxlbrightness[1] = -1; // pixel is actually out of matrix boundaries, do not render + } + if (particlesettings.wrapX) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlbrightness[0] = -1; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > maxXpixel) // right pixel, only has to be checkt if left pixel did not overflow + { + if (particlesettings.wrapX) // wrap y to the other side if required + pixco[1] = 0; + else + pxlbrightness[1] = -1; + } + + // calculate brightness values for the two pixels representing a particle using linear interpolation + + //calculate the values for pixels that are in frame + if (pxlbrightness[0] >= 0) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + if (pxlbrightness[1] >= 0) + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) + { + if (renderbuffer && framebuffer) + { + memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels + } + else + return; // cannot render advanced particles without buffer + + + //render particle to a bigger size + //particle size to pixels: < 64 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t size = advPartProps[particleindex].size; + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for(int i = 0; i < blurpasses; i++) + { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, true, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for(uint32_t xrb = offset; xrb < rendersize+offset; xrb++) + { + xfb = xfb_orig + xrb; + if (xfb > maxXpixel) + { + if (particlesettings.wrapX) // wrap x to the other side if required + { + if (xfb > maxXpixel << 1) // xfb is "negative" (note: for some reason, this check is needed in 1D but works without in 2D...) + xfb = (maxXpixel +1) + (int32_t)xfb; + else + xfb = xfb % (maxXpixel + 1); + } + else + continue; + } + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + } + } + else if (framebuffer) // standard rendering (2 pixels per particle) + { + for(uint32_t i = 0; i < 2; i++) + { + if (pxlbrightness[i] > 0) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else + { + for(uint32_t i = 0; i < 2; i++) + { + if (pxlbrightness[i] > 0) + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i])); + } + } + } +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() +{ + // detect and handle collisions + uint32_t i, j; + uint32_t startparticle = 0; + uint32_t endparticle = usedParticles;// >> 1; // do half the particles, significantly speeds things up + // every second frame, do other half of particles (helps to speed things up as not all collisions are handled each frame, less accurate but good enough) + // if more accurate collisions are needed, just call it twice in a row + /*if (SEGMENT.call & 0x01) //every other frame, do the other half + { + startparticle = endparticle; + endparticle = usedParticles; + } */ + int32_t collisiondistance = PS_P_MINHARDRADIUS_1D; + + for (i = startparticle; i < endparticle; i++) + { + // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + if (particles[i].ttl > 0 && particles[i].outofbounds == 0 && particles[i].collide) // if particle is alive and does collide and is not out of view + { + int32_t dx; // distance to other particles + for (j = i + 1; j < usedParticles; j++) // check against higher number particles + { + if (particles[j].ttl > 0 && particles[j].collide) // if target particle is alive + { + if (advPartProps) // use advanced size properties + { + collisiondistance += ((uint32_t)advPartProps[i].size + (uint32_t)advPartProps[j].size)>>1; + } + dx = particles[j].x - particles[i].x; + int32_t dv = (int32_t)particles[j].vx - (int32_t)particles[i].vx; + int32_t proximity = collisiondistance; + if (dv >= proximity) //particles would go past each other in next move upate + proximity += abs(dv); //add speed difference to catch fast particles + if (dx < proximity && dx > -proximity) // check if close + { + collideParticles(&particles[i], &particles[j], dx, dv, collisiondistance); + } + } + } + } + } +} + +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +void ParticleSystem1D::collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeVx, uint32_t collisiondistance) +{ + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other + // int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + //Serial.print(" dp"); Serial.print(dotProduct); + if (dotProduct < 0) // particles are moving towards each other + { + // integer math used to avoid floats. + // Calculate new velocities after collision + uint32_t surfacehardness = collisionHardness < PS_P_MINSURFACEHARDNESS_1D ? PS_P_MINSURFACEHARDNESS_1D : collisionHardness; // if particles are soft, the impulse must stay above a limit or collisions slip through + //TODO: if soft collisions are not needed, the above line can be done in set hardness function and skipped here (which is what it currently looks like) + + int32_t impulse = relativeVx * surfacehardness / 255; + particle1->vx += impulse; + particle2->vx -= impulse; + + //if one of the particles is fixed, transfer the impulse back so it bounces + if (particle1->fixed) + particle2->vx = -particle1->vx; + else if (particle2->fixed) + particle1->vx = -particle2->vx; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D) // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and correctly) + { + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS_1D); + particle1->vx = ((int32_t)particle1->vx * coeff) / 255; + particle2->vx = ((int32_t)particle2->vx * coeff) / 255; + } + } + + uint32_t distance = abs(dx); + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle to full radius (direction is well defined in 1D) + // also need to give the top particle some speed to counteract gravity or stacks just collapse + if (distance < collisiondistance) //particles are too close, push the upper particle away + { + int32_t pushamount = 1 + ((collisiondistance - distance) >> 1); //add half the remaining distance note: this works best, if less or more is added, it gets more chaotic + //int32_t pushamount = collisiondistance - distance; + if (particlesettings.useGravity) //using gravity, push the 'upper' particle only + { + if (dx < 0) // particle2.x < particle1.x + { + if (particle2->reversegrav && !particle2->fixed) + { + particle2->x -= pushamount; + particle2->vx--; + } + else if (!particle1->reversegrav && !particle1->fixed) + { + particle1->x += pushamount; + particle1->vx++; + } + } + else + { + if (particle1->reversegrav && !particle1->fixed) + { + particle1->x -= pushamount; + particle1->vx--; + } + else if (!particle2->reversegrav && !particle2->fixed) + { + particle2->x += pushamount; + particle2->vx++; + } + } + } + else //not using gravity, push both particles by applying a little velocity (like in 2D system), results in much nicer stacking when applying forces + { + pushamount = 1; + if (dx < 0) // particle2.x < particle1.x + pushamount = -1; + + particle1->vx -= pushamount; + particle2->vx += pushamount; + } + } +} + + + +// allocate memory for the 1D array in one contiguous block and set values to zero +CRGB *ParticleSystem1D::allocate1Dbuffer(uint32_t length) +{ + CRGB *array = (CRGB *)calloc(length, sizeof(CRGB)); + //if (array == NULL) + // DEBUG_PRINT(F("PS 1D buffer alloc failed")); + return array; +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGMENT.data) +void ParticleSystem1D::updateSystem(void) +{ + // update size + setSize(SEGMENT.virtualLength()); + updatePSpointers(advPartProps != NULL); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem1D::updatePSpointers(bool isadvanced) +{ + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + particles = reinterpret_cast(this + 1); // pointer to particle array at data+sizeof(ParticleSystem) + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + if (isadvanced) + { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + //if (sizecontrol) + //{ + // advPartSize = reinterpret_cast(advPartProps + numParticles); + // PSdataEnd = reinterpret_cast(advPartSize + numParticles); + //} + } + + /* + DEBUG_PRINTF_P(PSTR(" particles %p "), particles); + DEBUG_PRINTF_P(PSTR(" sources %p "), sources); + DEBUG_PRINTF_P(PSTR(" adv. props %p "), advPartProps); + DEBUG_PRINTF_P(PSTR(" adv. ctrl %p "), advPartSize); + DEBUG_PRINTF_P(PSTR("end %p\n"), PSdataEnd); + */ +} + + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles1D(bool isadvanced) +{ + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed +#endif + numberofParticles = max((uint32_t)1, min(numberofParticles, particlelimit)); + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(uint8_t requestedsources) +{ +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 +#endif + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes) +{ + uint32_t requiredmemory = sizeof(ParticleSystem1D); + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticle1D) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; + requiredmemory += additionalbytes; + //Serial.print("allocating: "); + //Serial.print(requiredmemory); + //Serial.println("Bytes"); + //Serial.print("allocating for segment at"); + //Serial.println((uintptr_t)SEGMENT.data); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes, bool advanced) +{ + //Serial.println("PS init function"); + uint32_t numparticles = calculateNumberOfParticles1D(advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + //Serial.print("numsources: "); + //Serial.println(numsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + //Serial.print("segment.data ptr"); + //Serial.println((uintptr_t)(SEGMENT.data)); + //Serial.println("calling constructor"); + PartSys = new (SEGMENT.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + //Serial.print("PS pointer at "); + //Serial.println((uintptr_t)PartSys); + return true; +} + + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates (default start is 0/0) +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + + carryover = BLACK; + for(uint32_t x = start; x < start + size; x++) + { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (!smear) // fade current pixel if smear is disabled + fast_color_scale(colorbuffer[x], 255 - blur); + + if (x > 0) + { + fast_color_add(colorbuffer[x-1], seeppart); + fast_color_add(colorbuffer[x], carryover); + } + carryover = seeppart; + } + fast_color_add(colorbuffer[size-1], carryover); // set last pixel +} + + +#endif // WLED_DISABLE_PARTICLESYSTEM1D + + +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +// force is in 3.4 fixedpoint notation, +/-127 +int32_t calcForce_dv(int8_t force, uint8_t* counter) +{ + if (force == 0) + return 0; + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) + { + *counter += force_abs; + if (*counter > 15) + { + *counter -= 16; + dv = force < 0 ? -1 : 1; // force is either, 1 or -1 if it is small (zero force is handled above) + } + } + else + { + dv = force / 16; // MSBs note: cannot use bitshift as dv can be negative + } + return dv; +} + +// limit speed to prevent overflows +int32_t limitSpeed(int32_t speed) +{ + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); +} + +// fastled color adding is very inaccurate in color preservation +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color (fastled scale8 can be made faster using native 32bit on ESP) +// note: result is stored in c1, so c1 will contain the result. not using a return value is much faster as the struct does not need to be copied upon return +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale) +{ + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } + else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + uint32_t max = r; + if (g > max) // note: using ? operator would be slower by 2 instructions + max = g; + if (b > max) + max = b; + if (max < 256) + { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } + else + { + c1.r = (r * 255) / max; + c1.g = (g * 255) / max; + c1.b = (b * 255) / max; + } +} + +// faster than fastled color scaling as it uses a 32bit scale factor and pointer +void fast_color_scale(CRGB &c, uint32_t scale) +{ + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} +#endif // !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 57a54e5406..f96b387e17 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -24,11 +24,23 @@ THE SOFTWARE. */ -#ifndef WLED_DISABLE_PARTICLESYSTEM +#if !defined(WLED_DISABLE_PARTICLESYSTEM2D) || !defined(WLED_DISABLE_PARTICLESYSTEM1D) #include #include "FastLED.h" +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) + + +//shared functions (used both in 1D and 2D system) +int32_t calcForce_dv(int8_t force, uint8_t *counter); //TODO: same as 2D function, could share +int32_t limitSpeed(int32_t speed); //TODO: same as 2D function, could share +void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 + +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation #define ESP8266_MAXPARTICLES 180 // enough for one 16x16 segment with transitions #define ESP8266_MAXSOURCES 16 @@ -44,7 +56,23 @@ #define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 #define PS_P_MINHARDRADIUS 70 // minimum hard surface radius #define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky -#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) + +// struct for PS settings (shared for 1D and 2D class) +typedef union +{ + struct{ + // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; //struct for a single particle (10 bytes) typedef struct { @@ -59,7 +87,7 @@ typedef struct { bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area bool collide : 1; // if set, particle takes part in collisions bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) - bool flag4 : 1; // unused flag + bool state : 1; //can be used by FX to track state, not used in PS } PSparticle; // struct for additional particle settings (optional) @@ -99,23 +127,6 @@ typedef struct { uint8_t size; // particle size (advanced property) } PSsource; -// struct for PS settings -typedef union -{ - struct{ - // one byte bit field: - bool wrapX : 1; - bool wrapY : 1; - bool bounceX : 1; - bool bounceY : 1; - bool killoutofbounds : 1; // if set, out of bound particles are killed immediately - bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top - bool useCollisions : 1; - bool colorByAge : 1; // if set, particle hue is set by ttl value in render function - }; - byte asByte; // order is: LSB is first entry in the list above -} PSsettings; - // class uses approximately 60 bytes class ParticleSystem { @@ -125,14 +136,14 @@ class ParticleSystem void update(void); //update the particles according to set options and render to the matrix void updateFire(uint32_t intensity, bool renderonly = false); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions - + void particleMoveUpdate(PSparticle &part, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters int32_t sprayEmit(PSsource &emitter, uint32_t amount = 1); void flameEmit(PSsource &emitter); void angleEmit(PSsource& emitter, uint16_t angle, int8_t speed, uint32_t amount = 1); - void particleMoveUpdate(PSparticle &part, PSsettings *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function //particle physics void applyGravity(PSparticle *part); // applies gravity to single particle (use this for sources) void applyForce(PSparticle *part, int8_t xforce, int8_t yforce, uint8_t *counter); @@ -147,7 +158,7 @@ class ParticleSystem void lineAttractor(uint16_t particleindex, PSparticle *attractorcenter, uint16_t attractorangle, uint8_t strength); // set options - void setUsedParticles(uint16_t num); + void setUsedParticles(uint32_t num); void setCollisionHardness(uint8_t hardness); // hardness for particle collisions (255 means full hard) void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setWallRoughness(uint8_t roughness); // wall roughness randomizes wall collisions @@ -169,16 +180,16 @@ class ParticleSystem PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data - uint16_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels - uint32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 - uint8_t numSources; // number of sources - uint16_t numParticles; // number of particles available in this system - uint16_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + uint32_t numParticles; // number of particles available in this system + uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) private: //rendering functions void ParticleSys_render(bool firemode = false, uint32_t fireintensity = 128); - void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightess, CRGB color, CRGB **renderbuffer); + void renderParticle(CRGB **framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB **renderbuffer); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles @@ -192,19 +203,17 @@ class ParticleSystem void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall int16_t wraparound(uint16_t p, uint32_t maxvalue); - int32_t calcForce_dv(int8_t force, uint8_t *counter); - int32_t limitSpeed(int32_t speed); CRGB **allocate2Dbuffer(uint32_t cols, uint32_t rows); // note: variables that are accessed often are 32bit for speed - PSsettings particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; - uint8_t wallHardness; - uint8_t wallRoughness; + uint32_t wallHardness; + uint32_t wallRoughness; uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) - uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? + uint32_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? uint8_t forcecounter; // counter for globally applied forces // global particle properties for basic particles uint8_t particlesize; // global particle size, 0 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) @@ -212,14 +221,164 @@ class ParticleSystem uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 }; -// initialization functions (not part of class) -bool initParticleSystem(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool largesizes = false, bool sizecontrol = false); -uint32_t calculateNumberOfParticles(bool advanced, bool sizecontrol); -uint32_t calculateNumberOfSources(uint8_t requestedsources); -bool allocateParticleSystemMemory(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); -// color functions -void fast_color_add(CRGB &c1, CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -void fast_color_scale(CRGB &c, uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 void blur2D(CRGB **colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, bool smear = true, uint32_t xstart = 0, uint32_t ystart = 0, bool isparticle = false); +// initialization functions (not part of class) +bool initParticleSystem2D(ParticleSystem *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false, bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(bool advanced, bool sizecontrol); +uint32_t calculateNumberOfSources2D(uint8_t requestedsources); +bool allocateParticleSystemMemory2D(uint16_t numparticles, uint16_t numsources, bool advanced, bool sizecontrol, uint16_t additionalbytes); + +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +//MAX_SEGMENT_DATA +#define ESP8266_MAXPARTICLES_1D 400 +#define ESP8266_MAXSOURCES_1D 8 +#define ESP32S2_MAXPARTICLES_1D 1900 +#define ESP32S2_MAXSOURCES_1D 16 +#define ESP32_MAXPARTICLES_1D 6000 +#define ESP32_MAXSOURCES_1D 32 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D 16 +#define PS_P_RADIUS_SHIFT_1D 5 //TODO: may need to adjust +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius +#define PS_P_MINSURFACEHARDNESS_1D 50 // minimum hardness used in collision impulse calculation + +// struct for PS settings (shared for 1D and 2D class) +typedef union +{ + struct{ + // one byte bit field for 1D settings + bool wrapX : 1; + bool bounceX : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + +//struct for a single particle (6 bytes) +typedef struct { + int16_t x; // x position in particle system + int8_t vx; // horizontal velocity + uint8_t hue; // color hue + // two byte bit field: + uint16_t ttl : 11; // time to live, 11 bit or 2047 max (which is 25s at 80FPS) + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool reversegrav : 1; // if set, gravity is reversed on this particle + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), +} PSparticle1D; + +// struct for additional particle settings (optional) +typedef struct +{ + uint8_t sat; //color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; +} PSadvancedParticle1D; + +//struct for a particle source (17 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) + uint8_t size; // particle size (advanced property) +} PSsource1D; + + +class ParticleSystem1D +{ +public: + ParticleSystem1D(uint16_t length, uint16_t numberofparticles, uint16_t numberofsources, bool isadvanced = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + + // particle emitters + int32_t sprayEmit(PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + //particle physics + void applyForce(PSparticle1D *part, int8_t xforce, uint8_t *counter); //apply a force to a single particle + void applyForce(int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D *part); // applies gravity to single particle (use this for sources) + void applyFriction(int32_t coefficient); // apply friction to all used particles + + // set options + void setUsedParticles(uint32_t num); + void setWallHardness(uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(uint16_t x); //set particle system size (= strip length) + void setWrap(bool enable); + void setBounce(bool enable); + void setKillOutOfBounds(bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(bool enable); + void setColorByPosition(bool enable); + void setMotionBlur(uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setParticleSize(uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, uint8_t hardness = 255); + + PSparticle1D *particles; // pointer to particle array + PSsource1D *sources; // pointer to sources + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX; // particle system size i.e. width-1 + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + uint32_t numParticles; // number of particles available in this system + uint32_t usedParticles; // number of particles used in animation (can be smaller then numParticles) + +private: + //rendering functions + void ParticleSys_render(void); + void renderParticle(CRGB *framebuffer, uint32_t particleindex, uint32_t brightness, CRGB color, CRGB *renderbuffer); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + void collideParticles(PSparticle1D *particle1, PSparticle1D *particle2, int32_t dx, int32_t relativeV, uint32_t collisiondistance); + + //utility functions + void updatePSpointers(bool isadvanced); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + //void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, uint16_t maxposition); // bounce on a wall + CRGB *allocate1Dbuffer(uint32_t length); + // note: variables that are accessed often are 32bit for speed + PSsettings1D particlesettings; // settings used when updating particles + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + //uint8_t collisioncounter; // counter to handle collisions TODO: could use the SEGMENT.call? -> currently unused + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, larger sizez TBD (TODO: need larger sizes?) + int32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 +}; -#endif // WLED_DISABLE_PARTICLESYSTEM +bool initParticleSystem1D(ParticleSystem1D *&PartSys, uint8_t requestedsources, uint16_t additionalbytes = 0, bool advanced = false); +uint32_t calculateNumberOfParticles1D(bool isadvanced); +uint32_t calculateNumberOfSources1D(uint8_t requestedsources); +bool allocateParticleSystemMemory1D(uint16_t numparticles, uint16_t numsources, bool isadvanced, uint16_t additionalbytes); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, bool smear = true, uint32_t start = 0); +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index bcca2e83fb..1f70bb257e 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -270,12 +270,6 @@ bool BusDigital::canShow() { void BusDigital::setBrightness(uint8_t b) { if (_bri == b) return; - //Fix for turning off onboard LED breaking bus - #ifdef LED_BUILTIN - if (_bri == 0) { // && b > 0, covered by guard if above - if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) reinit(); - } - #endif Bus::setBrightness(b); PolyBus::setBrightness(_busPtr, _iType, b); } @@ -391,14 +385,14 @@ BusPwm::BusPwm(BusConfig &bc) uint8_t numPins = NUM_PWM_PINS(bc.type); _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; - #ifdef ESP8266 +#ifdef ESP8266 // duty cycle resolution (_depth) can be extracted from this formula: 1MHz > _frequency * 2^_depth if (_frequency > 1760) _depth = 8; else if (_frequency > 880) _depth = 9; else _depth = 10; // WLED_PWM_FREQ <= 880Hz analogWriteRange((1<<_depth)-1); analogWriteFreq(_frequency); - #else +#else _ledcStart = pinManager.allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels deallocatePins(); return; @@ -408,7 +402,7 @@ BusPwm::BusPwm(BusConfig &bc) else if (_frequency > 39062) _depth = 10; else if (_frequency > 19531) _depth = 11; else _depth = 12; // WLED_PWM_FREQ <= 19531Hz - #endif +#endif for (unsigned i = 0; i < numPins; i++) { uint8_t currentPin = bc.pins[i]; @@ -685,11 +679,11 @@ uint32_t BusManager::memUsage(BusConfig &bc) { if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem multiplier = 5; } - #else //ESP32 RMT uses double buffer, I2S uses 5x buffer - multiplier = 2; + #else //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) + multiplier = PolyBus::isParallelI2S1Output() ? 24 : 2; #endif } - return len * channels * multiplier; //RGB + return (len * multiplier + bc.doubleBuffer * (bc.count + bc.skipAmount)) * channels; } int BusManager::add(BusConfig &bc) { @@ -706,6 +700,11 @@ int BusManager::add(BusConfig &bc) { return numBusses++; } +void BusManager::useParallelOutput(void) { + _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods + PolyBus::setParallelI2S1Output(); +} + //do not call this method from system context (network callback) void BusManager::removeAll() { DEBUG_PRINTLN(F("Removing all.")); @@ -713,6 +712,79 @@ void BusManager::removeAll() { while (!canAllShow()) yield(); for (unsigned i = 0; i < numBusses; i++) delete busses[i]; numBusses = 0; + _parallelOutputs = 1; + PolyBus::setParallelI2S1Output(false); +} + +#ifdef ESP32_DATA_IDLE_HIGH +// #2478 +// If enabled, RMT idle level is set to HIGH when off +// to prevent leakage current when using an N-channel MOSFET to toggle LED power +void BusManager::esp32RMTInvertIdle() { + bool idle_out; + unsigned rmt = 0; + for (unsigned u = 0; u < numBusses(); u++) { + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, only has 1 I2S but NPB does not support it ATM + if (u > 1) return; + rmt = u; + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, only has 1 I2S bus, supported in NPB + if (u > 3) return; + rmt = u; + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, has 2 I2S but NPB does not support them ATM + if (u > 3) return; + rmt = u; + #else + if (u < _parallelOutputs) continue; + if (u >= _parallelOutputs + 8) return; // only 8 RMT channels + rmt = u - _parallelOutputs; + #endif + if (busses[u]->getLength()==0 || !IS_DIGITAL(busses[u]->getType()) || IS_2PIN(busses[u]->getType())) continue; + //assumes that bus number to rmt channel mapping stays 1:1 + rmt_channel_t ch = static_cast(rmt); + rmt_idle_level_t lvl; + rmt_get_idle_level(ch, &idle_out, &lvl); + if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; + else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; + else continue; + rmt_set_idle_level(ch, idle_out, lvl); + } +} +#endif + +void BusManager::on() { + #ifdef ESP8266 + //Fix for turning off onboard LED breaking bus + if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + for (unsigned i = 0; i < numBusses; i++) { + uint8_t pins[2] = {255,255}; + if (IS_DIGITAL(busses[i]->getType()) && busses[i]->getPins(pins)) { + if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { + BusDigital *bus = static_cast(busses[i]); + bus->reinit(); + break; + } + } + } + } + #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif +} + +void BusManager::off() { + #ifdef ESP8266 + // turn off built-in LED if strip is turned off + // this will break digital bus so will need to be re-initialised on On + if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, HIGH); + } + #endif + #ifdef ESP32_DATA_IDLE_HIGH + esp32RMTInvertIdle(); + #endif } void BusManager::show() { @@ -781,6 +853,8 @@ uint16_t BusManager::getTotalLength() { return len; } +bool PolyBus::useParallelI2S = false; + // Bus static member definition int16_t Bus::_cct = -1; uint8_t Bus::_cctBlend = 0; @@ -792,4 +866,5 @@ uint8_t BusManager::numBusses = 0; Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; ColorOrderMap BusManager::colorOrderMap = {}; uint16_t BusManager::_milliAmpsUsed = 0; -uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; \ No newline at end of file +uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; +uint8_t BusManager::_parallelOutputs = 1; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index c128f8c099..e2c4239d84 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -21,10 +21,6 @@ uint16_t approximateKelvinFromRGB(uint32_t rgb); #define IC_INDEX_WS2812_2CH_3X(i) ((i)*2/3) #define WS2812_2CH_3X_SPANS_2_ICS(i) ((i)&0x01) // every other LED zone is on two different ICs -// flag for using double buffering in BusDigital -extern bool useGlobalLedBuffer; - - //temporary struct for passing bus configuration to bus struct BusConfig { uint8_t type; @@ -41,7 +37,7 @@ struct BusConfig { uint8_t milliAmpsPerLed; uint16_t milliAmpsMax; - BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=55, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, bool dblBfr=false, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) : count(len) , start(pstart) , colorOrder(pcolorOrder) @@ -133,7 +129,7 @@ class Bus { virtual uint32_t getPixelColor(uint16_t pix) { return 0; } virtual void setBrightness(uint8_t b) { _bri = b; }; virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return _len; } + virtual uint16_t getLength() { return isOk() ? _len : 0; } virtual void setColorOrder(uint8_t co) {} virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } virtual uint8_t skippedLeds() { return 0; } @@ -173,10 +169,11 @@ class Bus { type == TYPE_FW1906 || type == TYPE_WS2805 ) return true; return false; } - static int16_t getCCT() { return _cct; } + static inline int16_t getCCT() { return _cct; } static void setCCT(int16_t cct) { _cct = cct; } + static inline uint8_t getCCTBlend() { return _cctBlend; } static void setCCTBlend(uint8_t b) { if (b > 100) b = 100; _cctBlend = (b * 127) / 100; @@ -363,10 +360,14 @@ class BusManager { static uint16_t ablMilliampsMax(void) { return _milliAmpsMax; } static int add(BusConfig &bc); + static void useParallelOutput(void); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) static void removeAll(); + static void on(void); + static void off(void); + static void show(); static bool canAllShow(); static void setStatusPixel(uint32_t c); @@ -394,7 +395,11 @@ class BusManager { static ColorOrderMap colorOrderMap; static uint16_t _milliAmpsUsed; static uint16_t _milliAmpsMax; + static uint8_t _parallelOutputs; + #ifdef ESP32_DATA_IDLE_HIGH + static void esp32RMTInvertIdle(); + #endif static uint8_t getNumVirtualBusses() { int j = 0; for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index aea8bed655..d619e85aff 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -17,11 +17,6 @@ #if !defined(WLED_NO_I2S1_PIXELBUS) && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2)) #define WLED_NO_I2S1_PIXELBUS #endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifdef WLED_USE_PARALLEL_I2S - #warning Use less than 300 pixels per bus. - #endif -#endif // temporary end //Hardware SPI Pins @@ -224,146 +219,59 @@ #ifdef ARDUINO_ARCH_ESP32 //RGB #define B_32_RN_NEO_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_NEO_3 NeoPixelBusLg -//#define B_32_I0_NEO_3 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_NEO_3 NeoPixelBusLg - #else -#define B_32_I1_NEO_3 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_NEO_3P NeoPixelBusLg // parallel I2S //RGBW #define B_32_RN_NEO_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_NEO_4 NeoPixelBusLg -//#define B_32_I0_NEO_4 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_NEO_4 NeoPixelBusLg - #else -#define B_32_I1_NEO_4 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_NEO_4P NeoPixelBusLg // parallel I2S //400Kbps #define B_32_RN_400_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_400_3 NeoPixelBusLg -//#define B_32_I0_400_3 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_400_3 NeoPixelBusLg - #else -#define B_32_I1_400_3 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_400_3P NeoPixelBusLg // parallel I2S //TM1814 (RGBW) #define B_32_RN_TM1_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM1_4 NeoPixelBusLg -//#define B_32_I0_TM1_4 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM1_4 NeoPixelBusLg - #else -#define B_32_I1_TM1_4 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_TM1_4P NeoPixelBusLg // parallel I2S //TM1829 (RGB) #define B_32_RN_TM2_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM2_3 NeoPixelBusLg -//#define B_32_I0_TM2_3 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM2_3 NeoPixelBusLg - #else -#define B_32_I1_TM2_3 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_TM2_3P NeoPixelBusLg // parallel I2S //UCS8903 #define B_32_RN_UCS_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_3 NeoPixelBusLg -//#define B_32_I0_UCS_3 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_UCS_3 NeoPixelBusLg - #else -#define B_32_I1_UCS_3 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_UCS_3P NeoPixelBusLg // parallel I2S //UCS8904 #define B_32_RN_UCS_4 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_UCS_4 NeoPixelBusLg -//#define B_32_I0_UCS_4 NeoPixelBusLg// parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_UCS_4 NeoPixelBusLg - #else -#define B_32_I1_UCS_4 NeoPixelBusLg// parallel I2S - #endif -#endif +#define B_32_I1_UCS_4P NeoPixelBusLg// parallel I2S +//APA106 #define B_32_RN_APA106_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_APA106_3 NeoPixelBusLg -//#define B_32_I0_APA106_3 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_APA106_3 NeoPixelBusLg - #else -#define B_32_I1_APA106_3 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_APA106_3P NeoPixelBusLg // parallel I2S //FW1906 GRBCW #define B_32_RN_FW6_5 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_FW6_5 NeoPixelBusLg -//#define B_32_I0_FW6_5 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_FW6_5 NeoPixelBusLg - #else -#define B_32_I1_FW6_5 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_FW6_5P NeoPixelBusLg // parallel I2S //WS2805 RGBWC #define B_32_RN_2805_5 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_2805_5 NeoPixelBusLg -//#define B_32_I0_2805_5 NeoPixelBusLg // parallel I2S -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_2805_5 NeoPixelBusLg - #else -#define B_32_I1_2805_5 NeoPixelBusLg // parallel I2S - #endif -#endif +#define B_32_I1_2805_5P NeoPixelBusLg // parallel I2S //TM1914 (RGB) #define B_32_RN_TM1914_3 NeoPixelBusLg -#ifndef WLED_NO_I2S0_PIXELBUS #define B_32_I0_TM1914_3 NeoPixelBusLg -//#define B_32_I0_TM1914_3 NeoPixelBusLg -#endif -#ifndef WLED_NO_I2S1_PIXELBUS - #ifndef WLED_USE_PARALLEL_I2S #define B_32_I1_TM1914_3 NeoPixelBusLg - #else -#define B_32_I1_TM1914_3 NeoPixelBusLg - #endif -#endif +#define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S #endif //APA102 @@ -401,7 +309,12 @@ //handles pointer type conversion for all possible bus types class PolyBus { + private: + static bool useParallelI2S; + public: + static inline void setParallelI2S1Output(bool b = true) { useParallelI2S = b; } + static inline bool isParallelI2S1Output(void) { return useParallelI2S; } // initialize SPI bus speed for DotStar methods template @@ -487,79 +400,46 @@ class PolyBus { case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->Begin(); break; - #endif case I_32_RN_NEO_4: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->Begin(); break; - #endif case I_32_RN_400_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->Begin(); break; - #endif case I_32_RN_TM1_4: beginTM1814(busPtr); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; + case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; + case I_32_RN_TM1914_3: beginTM1914(busPtr); break; + // I2S1 bus or parellel buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_TM1_4: if (useParallelI2S) beginTM1814(busPtr); else beginTM1814(busPtr); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Begin(); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->Begin(); break; + case I_32_I0_400_3: (static_cast(busPtr))->Begin(); break; case I_32_I0_TM1_4: beginTM1814(busPtr); break; case I_32_I0_TM2_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: beginTM1814(busPtr); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_UCS_4: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_FW6_5: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: (static_cast(busPtr))->Begin(); break; - #endif - case I_32_RN_TM1914_3: beginTM1914(busPtr); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: beginTM1914(busPtr); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: beginTM1914(busPtr); break; - #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; case I_HS_LPD_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -579,11 +459,8 @@ class PolyBus { #if defined(ARDUINO_ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C3)) // NOTE: "channel" is only used on ESP32 (and its variants) for RMT channel allocation // since 0.15.0-b3 I2S1 is favoured for classic ESP32 and moved to position 0 (channel 0) so we need to subtract 1 for correct RMT allocation - #ifdef WLED_USE_PARALLEL_I2S - if (channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 - #else - if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 - #endif + if (useParallelI2S && channel > 7) channel -= 8; // accommodate parallel I2S1 which is used 1st on classic ESP32 + else if (channel > 0) channel--; // accommodate I2S1 which is used as 1st bus on classic ESP32 #endif void* busPtr = nullptr; switch (busType) { @@ -635,79 +512,46 @@ class PolyBus { case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: busPtr = new B_32_RN_NEO_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: busPtr = new B_32_I1_NEO_3(len, pins[0]); break; - #endif case I_32_RN_NEO_4: busPtr = new B_32_RN_NEO_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: busPtr = new B_32_I1_NEO_4(len, pins[0]); break; - #endif case I_32_RN_400_3: busPtr = new B_32_RN_400_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: busPtr = new B_32_I1_400_3(len, pins[0]); break; - #endif case I_32_RN_TM1_4: busPtr = new B_32_RN_TM1_4(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM2_3: busPtr = new B_32_RN_TM2_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; + case I_32_I1_NEO_4: if (useParallelI2S) busPtr = new B_32_I1_NEO_4P(len, pins[0]); else busPtr = new B_32_I1_NEO_4(len, pins[0]); break; + case I_32_I1_400_3: if (useParallelI2S) busPtr = new B_32_I1_400_3P(len, pins[0]); else busPtr = new B_32_I1_400_3(len, pins[0]); break; + case I_32_I1_TM1_4: if (useParallelI2S) busPtr = new B_32_I1_TM1_4P(len, pins[0]); else busPtr = new B_32_I1_TM1_4(len, pins[0]); break; + case I_32_I1_TM2_3: if (useParallelI2S) busPtr = new B_32_I1_TM2_3P(len, pins[0]); else busPtr = new B_32_I1_TM2_3(len, pins[0]); break; + case I_32_I1_UCS_3: if (useParallelI2S) busPtr = new B_32_I1_UCS_3P(len, pins[0]); else busPtr = new B_32_I1_UCS_3(len, pins[0]); break; + case I_32_I1_UCS_4: if (useParallelI2S) busPtr = new B_32_I1_UCS_4P(len, pins[0]); else busPtr = new B_32_I1_UCS_4(len, pins[0]); break; + case I_32_I1_APA106_3: if (useParallelI2S) busPtr = new B_32_I1_APA106_3P(len, pins[0]); else busPtr = new B_32_I1_APA106_3(len, pins[0]); break; + case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; + case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; + case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: busPtr = new B_32_I0_NEO_3(len, pins[0]); break; + case I_32_I0_NEO_4: busPtr = new B_32_I0_NEO_4(len, pins[0]); break; + case I_32_I0_400_3: busPtr = new B_32_I0_400_3(len, pins[0]); break; case I_32_I0_TM1_4: busPtr = new B_32_I0_TM1_4(len, pins[0]); break; case I_32_I0_TM2_3: busPtr = new B_32_I0_TM2_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: busPtr = new B_32_I1_TM1_4(len, pins[0]); break; - case I_32_I1_TM2_3: busPtr = new B_32_I1_TM2_3(len, pins[0]); break; - #endif - case I_32_RN_UCS_3: busPtr = new B_32_RN_UCS_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: busPtr = new B_32_I0_UCS_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: busPtr = new B_32_I1_UCS_3(len, pins[0]); break; - #endif - case I_32_RN_UCS_4: busPtr = new B_32_RN_UCS_4(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: busPtr = new B_32_I0_UCS_4(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: busPtr = new B_32_I1_UCS_4(len, pins[0]); break; - #endif - case I_32_RN_APA106_3: busPtr = new B_32_RN_APA106_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: busPtr = new B_32_I0_APA106_3(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: busPtr = new B_32_I1_APA106_3(len, pins[0]); break; - #endif - case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: busPtr = new B_32_I1_FW6_5(len, pins[0]); break; - #endif - case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: busPtr = new B_32_I1_2805_5(len, pins[0]); break; - #endif - case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; - #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) case I_HS_DOT_3: busPtr = new B_HS_DOT_3(len, pins[1], pins[0]); break; @@ -775,79 +619,46 @@ class PolyBus { case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->Show(consistent); break; - #endif case I_32_RN_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->Show(consistent); break; - #endif case I_32_RN_400_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->Show(consistent); break; - #endif case I_32_RN_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM2_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_400_3: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_TM1_4: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_TM2_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->Show(consistent); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_APA106_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: (static_cast(busPtr))->Show(consistent); break; - #endif - case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; - #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; case I_SS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -911,78 +722,45 @@ class PolyBus { case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 - case I_32_RN_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_NEO_4: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_400_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_RN_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I0_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: return (static_cast(busPtr))->CanShow(); break; - case I_32_I1_TM2_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_FW6_5: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_FW6_5: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_2805_5: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_2805_5: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: return (static_cast(busPtr))->CanShow(); break; - #endif - case I_32_RN_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_TM1914_3: return (static_cast(busPtr))->CanShow(); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + // RMT buses + case I_32_RN_NEO_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_NEO_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_400_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM2_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_2805_5: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->CanShow(); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + #endif + // I2S0 bus + #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_400_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM2_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_UCS_4: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_APA106_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_FW6_5: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_2805_5: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_TM1914_3: (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -1073,79 +851,46 @@ class PolyBus { case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif case I_32_RN_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - #endif case I_32_RN_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif case I_32_RN_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_32_I0_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; case I_32_I0_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetPixelColor(pix, col); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; - #endif - case I_32_RN_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; - #endif - case I_32_RN_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif - case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #endif - case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - #endif - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_SS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -1210,79 +955,46 @@ class PolyBus { case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif case I_32_RN_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif case I_32_RN_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif case I_32_RN_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_NEO_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_400_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM2_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_NEO_4: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_400_3: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: (static_cast(busPtr))->SetLuminance(b); break; - case I_32_I1_TM2_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: (static_cast(busPtr))->SetLuminance(b); break; - #endif - case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; - #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; case I_SS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1348,79 +1060,46 @@ class PolyBus { case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif case I_32_RN_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif case I_32_RN_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif case I_32_RN_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_RN_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_NEO_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; + case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_32_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_FW6_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I1_2805_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_32_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_NEO_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - case I_32_RN_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - #endif - case I_32_RN_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; - #endif - case I_32_RN_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif - case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W - #endif - case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_SS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1504,79 +1183,46 @@ class PolyBus { case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 + // RMT buses case I_32_RN_NEO_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_3: delete (static_cast(busPtr)); break; - #endif case I_32_RN_NEO_4: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_NEO_4: delete (static_cast(busPtr)); break; - #endif case I_32_RN_400_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS - case I_32_I0_400_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_400_3: delete (static_cast(busPtr)); break; - #endif case I_32_RN_TM1_4: delete (static_cast(busPtr)); break; case I_32_RN_TM2_3: delete (static_cast(busPtr)); break; + case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; + case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; + case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; + case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; + case I_32_RN_2805_5: delete (static_cast(busPtr)); break; + case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; + // I2S1 bus or paralell buses + #ifndef WLED_NO_I2S1_PIXELBUS + case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_NEO_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_400_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM1_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM2_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_UCS_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_UCS_4: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_APA106_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + #endif + // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS + case I_32_I0_NEO_3: delete (static_cast(busPtr)); break; + case I_32_I0_NEO_4: delete (static_cast(busPtr)); break; + case I_32_I0_400_3: delete (static_cast(busPtr)); break; case I_32_I0_TM1_4: delete (static_cast(busPtr)); break; case I_32_I0_TM2_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1_4: delete (static_cast(busPtr)); break; - case I_32_I1_TM2_3: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_UCS_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_3: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_UCS_4: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_UCS_4: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_UCS_4: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_APA106_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_APA106_3: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_APA106_3: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_FW6_5: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_2805_5: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_2805_5: delete (static_cast(busPtr)); break; - #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_2805_5: delete (static_cast(busPtr)); break; - #endif - case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; - #ifndef WLED_NO_I2S0_PIXELBUS case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; #endif - #ifndef WLED_NO_I2S1_PIXELBUS - case I_32_I1_TM1914_3: delete (static_cast(busPtr)); break; - #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; case I_SS_DOT_3: delete (static_cast(busPtr)); break; @@ -1661,15 +1307,15 @@ class PolyBus { //if (num > 3) offset = num -4; // I2S not supported yet #else // standard ESP32 has 8 RMT and 2 I2S channels - #ifdef WLED_USE_PARALLEL_I2S - if (num > 16) return I_NONE; - if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels - if (num == 16) offset = 1; - #else - if (num > 9) return I_NONE; - if (num > 8) offset = 1; - if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) - #endif + if (useParallelI2S) { + if (num > 16) return I_NONE; + if (num < 8) offset = 2; // prefer 8 parallel I2S1 channels + if (num == 16) offset = 1; + } else { + if (num > 9) return I_NONE; + if (num > 8) offset = 1; + if (num == 0) offset = 2; // prefer I2S1 for 1st bus (less flickering but more RAM needed) + } #endif switch (busType) { case TYPE_WS2812_1CH_X3: diff --git a/wled00/button.cpp b/wled00/button.cpp index 6d69f15f80..519cd08e1e 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -358,69 +358,35 @@ void handleButton() } } -// If enabled, RMT idle level is set to HIGH when off -// to prevent leakage current when using an N-channel MOSFET to toggle LED power -#ifdef ESP32_DATA_IDLE_HIGH -void esp32RMTInvertIdle() -{ - bool idle_out; - for (uint8_t u = 0; u < BusManager::getNumBusses(); u++) - { - if (u > 7) return; // only 8 RMT channels, TODO: ESP32 variants have less RMT channels - Bus *bus = BusManager::getBus(u); - if (!bus || bus->getLength()==0 || !IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType())) continue; - //assumes that bus number to rmt channel mapping stays 1:1 - rmt_channel_t ch = static_cast(u); - rmt_idle_level_t lvl; - rmt_get_idle_level(ch, &idle_out, &lvl); - if (lvl == RMT_IDLE_LEVEL_HIGH) lvl = RMT_IDLE_LEVEL_LOW; - else if (lvl == RMT_IDLE_LEVEL_LOW) lvl = RMT_IDLE_LEVEL_HIGH; - else continue; - rmt_set_idle_level(ch, idle_out, lvl); - } -} -#endif - +// handleIO() happens *after* handleTransitions() (see wled.cpp) which may change bri/briT but *before* strip.service() +// where actual LED painting occurrs +// this is important for relay control and in the event of turning off on-board LED void handleIO() { handleButton(); - //set relay when LEDs turn on - if (strip.getBrightness()) - { + // if we want to control on-board LED (ESP8266) or relay we have to do it here as the final show() may not happen until + // next loop() cycle + if (strip.getBrightness()) { lastOnTime = millis(); - if (offMode) - { - #ifdef ESP32_DATA_IDLE_HIGH - esp32RMTInvertIdle(); - #endif + if (offMode) { + BusManager::on(); if (rlyPin>=0) { pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, rlyMde); } offMode = false; } - } else if (millis() - lastOnTime > 600) - { + } else if (millis() - lastOnTime > 600 && !strip.needsUpdate()) { + // for turning LED or relay off we need to wait until strip no longer needs updates (strip.trigger()) if (!offMode) { - #ifdef ESP8266 - // turn off built-in LED if strip is turned off - // this will break digital bus so will need to be re-initialised on On - PinOwner ledPinOwner = pinManager.getPinOwner(LED_BUILTIN); - if (!strip.isOffRefreshRequired() && (ledPinOwner == PinOwner::None || ledPinOwner == PinOwner::BusDigital)) { - pinMode(LED_BUILTIN, OUTPUT); - digitalWrite(LED_BUILTIN, HIGH); - } - #endif - #ifdef ESP32_DATA_IDLE_HIGH - esp32RMTInvertIdle(); - #endif + BusManager::off(); if (rlyPin>=0) { pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); digitalWrite(rlyPin, !rlyMde); } + offMode = true; } - offMode = true; } } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 1ab1cba72c..57ea556b34 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -79,15 +79,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { getStringFromJson(apSSID, ap[F("ssid")], 33); getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security //int ap_pskl = ap[F("pskl")]; - CJSON(apChannel, ap[F("chan")]); if (apChannel > 13 || apChannel < 1) apChannel = 1; - CJSON(apHide, ap[F("hide")]); if (apHide > 1) apHide = 1; - CJSON(apBehavior, ap[F("behav")]); - /* JsonArray ap_ip = ap["ip"]; for (byte i = 0; i < 4; i++) { @@ -95,9 +91,14 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } */ - noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted - noWifiSleep = !noWifiSleep; - force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g? + JsonObject wifi = doc[F("wifi")]; + noWifiSleep = !(wifi[F("sleep")] | !noWifiSleep); // inverted + //noWifiSleep = !noWifiSleep; + CJSON(force802_3g, wifi[F("phy")]); //force phy mode g? +#ifdef ARDUINO_ARCH_ESP32 + CJSON(txPower, wifi[F("txpwr")]); + txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm); +#endif JsonObject hw = doc[F("hw")]; @@ -156,18 +157,42 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray ins = hw_led["ins"]; if (fromFS || !ins.isNull()) { + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); int s = 0; // bus iterator if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - uint32_t mem = 0, globalBufMem = 0; - uint16_t maxlen = 0; + uint32_t mem = 0; bool busesChanged = false; + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) + bool useParallel = false; + #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) + unsigned digitalCount = 0; + unsigned maxLeds = 0; + int oldType = 0; + int j = 0; + for (JsonObject elm : ins) { + unsigned type = elm["type"] | TYPE_WS2812_RGB; + unsigned len = elm["len"] | 30; + if (IS_DIGITAL(type) && !IS_2PIN(type)) digitalCount++; + if (len > maxLeds) maxLeds = len; + // we need to have all LEDs of the same type for parallel + if (j++ < 8 && oldType > 0 && oldType != type) oldType = -1; + else if (oldType == 0) oldType = type; + } + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\nDifferent types: %d\n"), maxLeds, digitalCount, (int)(oldType == -1)); + // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 + if (/*oldType != -1 && */maxLeds <= 300 && digitalCount > 5) { + useParallel = true; + BusManager::useParallelOutput(); + DEBUG_PRINTF_P(PSTR("Switching to parallel I2S with max. %d LEDs per ouptut.\n"), maxLeds); + } + #endif for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; pins[0] = pinArr[0]; - uint8_t i = 0; + unsigned i = 0; for (int p : pinArr) { pins[i++] = p; if (i>4) break; @@ -193,12 +218,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh if (fromFS) { BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - mem += BusManager::memUsage(bc); - if (useGlobalLedBuffer && start + length > maxlen) { - maxlen = start + length; - globalBufMem = maxlen * 4; - } - if (mem + globalBufMem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() + if (useParallel && s < 8) { + // we are using parallel I2S and memUsage() will include x8 allocation into account + if (s == 0) + mem = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S + else + if (BusManager::memUsage(bc) > mem) + mem = BusManager::memUsage(bc); // if we have unequal LED count use the largest + } else + mem += BusManager::memUsage(bc); // includes global buffer + if (mem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); @@ -206,6 +235,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } s++; } + DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); + DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); doInitBusses = busesChanged; // finalization done in beginStrip() } @@ -215,7 +246,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { ColorOrderMap com = {}; - uint8_t s = 0; + unsigned s = 0; for (JsonObject entry : hw_com) { if (s > WLED_MAX_COLOR_ORDER_MAPPINGS) break; uint16_t start = entry["start"] | 0; @@ -234,10 +265,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { disablePullUp = !pull; JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { - for (uint8_t b = 0; b < WLED_MAX_BUTTONS; b++) { // deallocate existing button pins - pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button - } - uint8_t s = 0; + // deallocate existing button pins + for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + unsigned s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); int8_t pin = btn["pin"][0] | -1; @@ -745,8 +775,11 @@ void serializeConfig() { JsonObject wifi = root.createNestedObject(F("wifi")); wifi[F("sleep")] = !noWifiSleep; wifi[F("phy")] = force802_3g; +#ifdef ARDUINO_ARCH_ESP32 + wifi[F("txpwr")] = txPower; +#endif - #ifdef WLED_USE_ETHERNET +#ifdef WLED_USE_ETHERNET JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { @@ -768,7 +801,7 @@ void serializeConfig() { break; } } - #endif +#endif JsonObject hw = root.createNestedObject(F("hw")); diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 30d2c069ec..55fb5d4770 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -208,13 +208,13 @@ CRGBPalette16 generateRandomPalette(void) //generate fully random palette void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb { - float h = ((float)hue)/65535.0f; + float h = ((float)hue)/10922.5f; // hue*6/65535 float s = ((float)sat)/255.0f; - int i = floorf(h*6); - float f = h * 6.0f - i; + int i = int(h); + float f = h - i; int p = int(255.0f * (1.0f-s)); - int q = int(255.0f * (1.0f-f*s)); - int t = int(255.0f * (1.0f-(1.0f-f)*s)); + int q = int(255.0f * (1.0f-s*f)); + int t = int(255.0f * (1.0f-s*(1.0f-f))); p = constrain(p, 0, 255); q = constrain(q, 0, 255); t = constrain(t, 0, 255); diff --git a/wled00/const.h b/wled00/const.h index 801d7e45dd..0ff70e47d5 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -46,40 +46,58 @@ #ifndef WLED_MAX_BUSSES #ifdef ESP8266 - #define WLED_MAX_BUSSES 3 + #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_ANALOG_CHANNELS 5 + #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB #define WLED_MIN_VIRTUAL_BUSSES 2 #else #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 3 // will allow 2 digital & 1 analog (or the other way around) + #define WLED_MAX_BUSSES 4 // will allow 2 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 2 + #define WLED_MAX_ANALOG_CHANNELS 6 #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) - #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog + #define WLED_MAX_BUSSES 7 // will allow 5 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 5 + #define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_MIN_VIRTUAL_BUSSES 3 #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB does not support them ATM - #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog + #define WLED_MAX_BUSSES 6 // will allow 4 digital & 2 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 4 + #define WLED_MAX_ANALOG_CHANNELS 8 #define WLED_MIN_VIRTUAL_BUSSES 4 #else // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #ifndef WLED_USE_PARALLEL_I2S - #define WLED_MAX_BUSSES 10 - #else - #define WLED_MAX_BUSSES 17 - #endif - #define WLED_MIN_VIRTUAL_BUSSES 0 + #define WLED_MAX_BUSSES 20 // will allow 17 digital & 3 analog RGB + #define WLED_MAX_DIGITAL_CHANNELS 17 + #define WLED_MAX_ANALOG_CHANNELS 10 + #define WLED_MIN_VIRTUAL_BUSSES 4 #endif #endif #else #ifdef ESP8266 - #if WLED_MAX_BUSES > 5 + #if WLED_MAX_BUSSES > 5 #error Maximum number of buses is 5. #endif + #ifndef WLED_MAX_ANALOG_CHANNELS + #error You must also define WLED_MAX_ANALOG_CHANNELS. + #endif + #ifndef WLED_MAX_DIGITAL_CHANNELS + #error You must also define WLED_MAX_DIGITAL_CHANNELS. + #endif #define WLED_MIN_VIRTUAL_BUSSES (5-WLED_MAX_BUSSES) #else - #if WLED_MAX_BUSES > 10 - #error Maximum number of buses is 10. + #if WLED_MAX_BUSSES > 20 + #error Maximum number of buses is 20. + #endif + #ifndef WLED_MAX_ANALOG_CHANNELS + #error You must also define WLED_MAX_ANALOG_CHANNELS. + #endif + #ifndef WLED_MAX_DIGITAL_CHANNELS + #error You must also define WLED_MAX_DIGITAL_CHANNELS. #endif - #define WLED_MIN_VIRTUAL_BUSSES (10-WLED_MAX_BUSSES) + #define WLED_MIN_VIRTUAL_BUSSES (20-WLED_MAX_BUSSES) #endif #endif @@ -181,6 +199,7 @@ #define USERMOD_ID_BME68X 49 //Usermod "usermod_bme68x.h #define USERMOD_ID_INA226 50 //Usermod "usermod_ina226.h" #define USERMOD_ID_AHT10 51 //Usermod "usermod_aht10.h" +#define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -196,9 +215,9 @@ #define CALL_MODE_INIT 0 //no updates on init, can be used to disable updates #define CALL_MODE_DIRECT_CHANGE 1 #define CALL_MODE_BUTTON 2 //default button actions applied to selected segments -#define CALL_MODE_NOTIFICATION 3 -#define CALL_MODE_NIGHTLIGHT 4 -#define CALL_MODE_NO_NOTIFY 5 +#define CALL_MODE_NOTIFICATION 3 //caused by incoming notification (UDP or DMX preset) +#define CALL_MODE_NIGHTLIGHT 4 //nightlight progress +#define CALL_MODE_NO_NOTIFY 5 //change state but do not send notifications (UDP) #define CALL_MODE_FX_CHANGED 6 //no longer used #define CALL_MODE_HUE 7 #define CALL_MODE_PRESET_CYCLE 8 //no longer used @@ -332,7 +351,7 @@ #define BTN_TYPE_TOUCH_SWITCH 9 //Ethernet board types -#define WLED_NUM_ETH_TYPES 12 +#define WLED_NUM_ETH_TYPES 13 #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -346,6 +365,7 @@ #define WLED_ETH_ABCWLEDV43ETH 9 #define WLED_ETH_SERG74 10 #define WLED_ETH_ESP32_POE_WROVER 11 +#define WLED_ETH_LILYGO_T_POE_PRO 12 //Hue error codes #define HUE_ERROR_INACTIVE 0 @@ -482,6 +502,16 @@ #endif #endif +#ifndef LED_MILLIAMPS_DEFAULT + #define LED_MILLIAMPS_DEFAULT 55 // common WS2812B +#else + #if LED_MILLIAMPS_DEFAULT < 1 || LED_MILLIAMPS_DEFAULT > 100 + #warning "Unusual LED mA current, overriding with default value." + #undef LED_MILLIAMPS_DEFAULT + #define LED_MILLIAMPS_DEFAULT 55 + #endif +#endif + // PWM settings #ifndef WLED_PWM_FREQ #ifdef ESP8266 diff --git a/wled00/data/index.css b/wled00/data/index.css index 6ac3f3a1f7..0952cca210 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -923,6 +923,7 @@ select.sel-p, select.sel-pl, select.sel-ple { margin: 5px 0; width: 100%; height: 40px; + padding: 0 20px 0 8px; } div.sel-p { position: relative; diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 4a532abb7a..86b3b18796 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -126,9 +126,10 @@
+ - +

Color palette

diff --git a/wled00/data/index.js b/wled00/data/index.js index 26d78b2848..adddc38fb9 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -282,12 +282,12 @@ function onLoad() // fill effect extra data array loadFXData(()=>{ // load and populate effects - loadFX(()=>{ + setTimeout(()=>{loadFX(()=>{ loadPalettesData(()=>{ requestJson();// will load presets and create WS if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); }); - }); + })},50); }); }); resetUtil(); @@ -588,7 +588,7 @@ function loadFXData(callback = null) fxdata = []; if (!retry) { retry = true; - setTimeout(loadFXData, 500); // retry + setTimeout(()=>{loadFXData(loadFX);}, 500); // retry } showToast(e, true); }) @@ -669,18 +669,15 @@ function parseInfo(i) { //syncTglRecv = i.str; maxSeg = i.leds.maxseg; pmt = i.fs.pmt; + if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; // do we have a matrix set-up mw = i.leds.matrix ? i.leds.matrix.w : 0; mh = i.leds.matrix ? i.leds.matrix.h : 0; isM = mw>0 && mh>0; if (!isM) { - //gId("filter0D").classList.remove('hide'); - //gId("filter1D").classList.add('hide'); gId("filter2D").classList.add('hide'); } else { - //gId("filter0D").classList.add('hide'); - //gId("filter1D").classList.remove('hide'); gId("filter2D").classList.remove('hide'); } // if (i.noaudio) { @@ -745,10 +742,10 @@ ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
${isM?'Start X':'Start LED'}
`; gId('kv').innerHTML = cn; // update all sliders in Info - for (let sd of (d.querySelectorAll('#kv .sliderdisplay')||[])) { + d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => { let s = sd.previousElementSibling; if (s) updateTrail(s); - } + }); } function populateSegments(s) @@ -895,8 +892,8 @@ function populateSegments(s) gId('segutil2').style.display = (segCount > 1) ? "block":"none"; // rsbtn parent if (Array.isArray(li.maps) && li.maps.length>1) { - let cont = `Ledmap: `; + for (const k of li.maps) cont += ``; cont += "
"; gId("ledmap").innerHTML = cont; gId("ledmap").classList.remove('hide'); @@ -991,13 +988,12 @@ function populatePalettes() function redrawPalPrev() { - let palettes = d.querySelectorAll('#pallist .lstI'); - for (var pal of (palettes||[])) { + d.querySelectorAll('#pallist .lstI').forEach((pal,i) =>{ let lP = pal.querySelector('.lstIprev'); if (lP) { lP.style = genPalPrevCss(pal.dataset.id); } - } + }); } function genPalPrevCss(id) @@ -1358,10 +1354,12 @@ function updateSelectedFx() } // hide 2D mapping and/or sound simulation options - var segs = gId("segcont").querySelectorAll(`div[data-map="map2D"]`); - for (const seg of segs) if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); - var segs = gId("segcont").querySelectorAll(`div[data-snd="si"]`); - for (const seg of segs) if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? + gId("segcont").querySelectorAll(`div[data-map="map2D"]`).forEach((seg)=>{ + if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide'); + }); + gId("segcont").querySelectorAll(`div[data-snd="si"]`).forEach((seg)=>{ + if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "? + }); } } @@ -1498,6 +1496,12 @@ function readState(s,command=false) if (s.error && s.error != 0) { var errstr = ""; switch (s.error) { + case 1: + errstr = "Denied!"; + break; + case 3: + errstr = "Buffer locked!"; + break; case 8: errstr = "Effect RAM depleted!"; break; @@ -1562,8 +1566,7 @@ function setEffectParameters(idx) var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(","); // set html slider items on/off - let sliders = d.querySelectorAll("#sliders .sliderwrap"); - sliders.forEach((slider, i)=>{ + d.querySelectorAll("#sliders .sliderwrap").forEach((slider, i)=>{ let text = slider.getAttribute("title"); if ((!controlDefined && i<((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i]!="")) { if (slOnOff.length>i && slOnOff[i]!="!") text = slOnOff[i]; @@ -1577,8 +1580,7 @@ function setEffectParameters(idx) if (slOnOff.length > 5) { // up to 3 checkboxes gId('fxopt').classList.remove('fade'); - let checks = d.querySelectorAll("#sliders .ochkl"); - checks.forEach((check, i)=>{ + d.querySelectorAll("#sliders .ochkl").forEach((check, i)=>{ let text = check.getAttribute("title"); if (5+i5+i && slOnOff[5+i]!="!") text = slOnOff[5+i]; @@ -1705,9 +1707,7 @@ function requestJson(command=null) fetch(getURL('/json/si'), { method: type, - headers: { - "Content-type": "application/json; charset=UTF-8" - }, + headers: {"Content-Type": "application/json; charset=UTF-8"}, body: req }) .then(res => { @@ -2025,7 +2025,7 @@ ${makePlSel(plJson[i].end?plJson[i].end:0, true)} `; if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) { content += `
Ledmap: 
"; } } @@ -2173,13 +2173,12 @@ function selGrp(g) { event.preventDefault(); event.stopPropagation(); - var sel = gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`); var obj = {"seg":[]}; for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":false}); - for (let s of (sel||[])) { + gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`).forEach((s)=>{ let i = parseInt(s.id.substring(3)); obj.seg[i] = {"id":i,"sel":true}; - } + }); if (obj.seg.length) requestJson(obj); } @@ -2691,7 +2690,9 @@ function setBalance(b) function rmtTgl(ip,i) { event.preventDefault(); event.stopPropagation(); - fetch(`http://${ip}/win&T=2`, {method: 'get'}) + fetch(`http://${ip}/win&T=2`, { + method: 'get' + }) .then((r)=>{ return r.text(); }) @@ -2783,21 +2784,23 @@ function loadPalettesData(callback = null) function getPalettesData(page, callback) { fetch(getURL(`/json/palx?page=${page}`), { - method: 'get', - headers: { - "Content-type": "application/json; charset=UTF-8" - } + method: 'get' }) .then(res => { if (!res.ok) showErrorToast(); return res.json(); }) .then(json => { + retry = false; palettesData = Object.assign({}, palettesData, json.p); - if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 50); + if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75); else callback(); }) .catch((error)=>{ + if (!retry) { + retry = true; + setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry + } showToast(error, true); }); } @@ -2820,9 +2823,9 @@ function search(field, listId = null) { const search = field.value !== ''; // restore default preset sorting if no search term is entered - if (listId === 'pcont' && !search) { - populatePresets(); - return; + if (!search) { + if (listId === 'pcont') { populatePresets(); return; } + if (listId === 'pallist') { populatePalettes(); return; } } // clear filter if searching in fxlist @@ -2833,15 +2836,15 @@ function search(field, listId = null) { // do not search if filter is active if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; - const listItems = gId(listId).querySelectorAll('.lstI'); // filter list items but leave (Default & Solid) always visible - for (i = (listId === 'pcont' ? 0 : 1); i < listItems.length; i++) { - const listItem = listItems[i]; + const listItems = gId("fxlist").querySelectorAll('.lstI'); + listItems.forEach((listItem,i)=>{ + if (listId!=='pcont' && i===0) return; const listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase(); const searchIndex = listItemName.indexOf(field.value.toUpperCase()); listItem.style.display = (searchIndex < 0) ? 'none' : ''; listItem.dataset.searchIndex = searchIndex; - } + }); // sort list items by search index and name const sortedListItems = Array.from(listItems).sort((a, b) => { @@ -2902,14 +2905,12 @@ function filterFx() { inputField.value = ''; inputField.focus(); clean(inputField.nextElementSibling); - const listItems = gId("fxlist").querySelectorAll('.lstI'); - for (let i = 1; i < listItems.length; i++) { - const listItem = listItems[i]; + gId("fxlist").querySelectorAll('.lstI').forEach((listItem,i) => { const listItemName = listItem.querySelector('.lstIname').innerText; let hide = false; gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = true; }); listItem.style.display = hide ? 'none' : ''; - } + }); } function preventBlur(e) { @@ -3060,6 +3061,7 @@ function size() function togglePcMode(fromB = false) { + let ap = (fromB && !lastinfo) || (lastinfo && lastinfo.wifi && lastinfo.wifi.ap); if (fromB) { pcModeA = !pcModeA; localStorage.setItem('pcm', pcModeA); @@ -3069,6 +3071,7 @@ function togglePcMode(fromB = false) if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size() if (pcMode) openTab(0, true); gId('buttonPcm').className = (pcMode) ? "active":""; + if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; sCol('--bh', gId('bot').clientHeight + "px"); _C.style.width = (pcMode || simplifiedUI)?'100%':'400%'; @@ -3094,8 +3097,7 @@ function mergeDeep(target, ...sources) function tooltip(cont=null) { - const elements = d.querySelectorAll((cont?cont+" ":"")+"[title]"); - elements.forEach((element)=>{ + d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ element.addEventListener("mouseover", ()=>{ // save title element.setAttribute("data-title", element.getAttribute("title")); @@ -3122,8 +3124,7 @@ function tooltip(cont=null) }); element.addEventListener("mouseout", ()=>{ - const tooltips = d.querySelectorAll('.tooltip'); - tooltips.forEach((tooltip)=>{ + d.querySelectorAll('.tooltip').forEach((tooltip)=>{ tooltip.classList.remove("visible"); d.body.removeChild(tooltip); }); diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index cffa73563d..164dc5a774 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -5,7 +5,8 @@ LED Settings