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);