diff --git a/platformio.ini b/platformio.ini index 8b5c11bd4..66bd09957 100644 --- a/platformio.ini +++ b/platformio.ini @@ -220,6 +220,8 @@ default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + bitbank2/AnimatedGIF@^1.4.7 + pixelmatix/GifDecoder@^1.1.0 ${env.lib_deps} # additional build flags for audioreactive AR_build_flags = -D USERMOD_AUDIOREACTIVE diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1f626bc9a..681f3d107 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -4465,6 +4465,16 @@ uint16_t mode_washing_machine(void) { static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!"; +/* + Image effect + Draws a .gif image from filesystem on the matrix/strip +*/ +uint16_t mode_image(void) { + renderImageToSegment(SEGMENT); + return FRAMETIME; +} +static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128"; + /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e @@ -8025,6 +8035,8 @@ void WS2812FX::setupEffectData() { 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_IMAGE, &mode_image, _data_FX_MODE_IMAGE); + 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); diff --git a/wled00/FX.h b/wled00/FX.h index 3aa19bc35..d72580935 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -180,7 +180,7 @@ #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) #define FX_MODE_RUNNING_DUAL 52 -// #define FX_MODE_HALLOWEEN 53 // removed in 0.14! +#define FX_MODE_IMAGE 53 // was Halloween before 0.14 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 7e0d6f480..59be67624 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -5,6 +5,8 @@ * All globally accessible functions are declared here */ +#include "FX.h" + //alexa.cpp #ifndef WLED_DISABLE_ALEXA void onAlexaChange(EspalexaDevice* dev); @@ -125,6 +127,16 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); +//image_loader.cpp +#ifndef WLED_DISABLE_GIF +bool fileSeekCallback(unsigned long position); +unsigned long filePositionCallback(void); +int fileReadCallback(void); +int fileReadBlockCallback(void * buffer, int numberOfBytes); +int fileSizeCallback(void); +byte renderImageToSegment(Segment &seg); +#endif + //improv.cpp enum ImprovRPCType { Command_Wifi = 0x01, @@ -162,7 +174,6 @@ void handleIR(); #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" -#include "FX.h" bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0); bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp new file mode 100644 index 000000000..500fbc2dd --- /dev/null +++ b/wled00/image_loader.cpp @@ -0,0 +1,133 @@ +#ifndef WLED_DISABLE_GIF + +#include "GifDecoder.h" +#include "wled.h" + +File file; +char lastFilename[34] = "/"; +GifDecoder<320,320,12,true>* decoder; +bool gifDecodeFailed = false; +long lastFrameDisplayTime = 0, currentFrameDelay = 0; + +bool fileSeekCallback(unsigned long position) { + return file.seek(position); +} + +unsigned long filePositionCallback(void) { + return file.position(); +} + +int fileReadCallback(void) { + return file.read(); +} + +int fileReadBlockCallback(void * buffer, int numberOfBytes) { + return file.read((uint8_t*)buffer, numberOfBytes); +} + +int fileSizeCallback(void) { + return file.size(); +} + +bool openGif(const char *filename) { + file = WLED_FS.open(filename, "r"); + + if (!file) return false; + return true; +} + +Segment* activeSeg; +uint16_t gifWidth, gifHeight; +uint16_t fillPixX, fillPixY; + +void screenClearCallback(void) { + activeSeg->fill(0); +} + +void updateScreenCallback(void) {} + +void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // simple nearest-neighbor scaling + int16_t outY = y * activeSeg->height() / gifHeight; + int16_t outX = x * activeSeg->width() / gifWidth; + // set multiple pixels if upscaling + for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { + for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); + } + } +} + +#define IMAGE_ERROR_NONE 0 +#define IMAGE_ERROR_NO_NAME 1 +#define IMAGE_ERROR_SEG_LIMIT 2 +#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 +#define IMAGE_ERROR_FILE_MISSING 4 +#define IMAGE_ERROR_DECODER_ALLOC 5 +#define IMAGE_ERROR_GIF_DECODE 6 +#define IMAGE_ERROR_FRAME_DECODE 7 +#define IMAGE_ERROR_PREV 255 + +// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment +byte renderImageToSegment(Segment &seg) { + if (!seg.name) return IMAGE_ERROR_NO_NAME; + if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time + activeSeg = &seg; + + if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image + strncpy(lastFilename +1, seg.name, 32); + gifDecodeFailed = false; + if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) { + gifDecodeFailed = true; + return IMAGE_ERROR_UNSUPPORTED_FORMAT; + } + if (file) file.close(); + openGif(lastFilename); + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + if (!decoder) decoder = new GifDecoder<320,320,12,true>(); + if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } + decoder->setScreenClearCallback(screenClearCallback); + decoder->setUpdateScreenCallback(updateScreenCallback); + decoder->setDrawPixelCallback(drawPixelCallback); + decoder->setFileSeekCallback(fileSeekCallback); + decoder->setFilePositionCallback(filePositionCallback); + decoder->setFileReadCallback(fileReadCallback); + decoder->setFileReadBlockCallback(fileReadBlockCallback); + decoder->setFileSizeCallback(fileSizeCallback); + Serial.println("Starting decoding"); + if(decoder->startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } + Serial.println("Decoding started"); + } + + if (gifDecodeFailed) return IMAGE_ERROR_PREV; + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } + + // speed 0 = half speed, 128 = normal, 255 = full FX FPS + // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast + uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; + + if((millis() - lastFrameDisplayTime) >= wait) { + decoder->getSize(&gifWidth, &gifHeight); + fillPixX = (seg.width()+(gifWidth-1)) / gifWidth; + fillPixY = (seg.height()+(gifHeight-1)) / gifHeight; + int result = decoder->decodeFrame(false); + if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } + long lastFrameDelay = currentFrameDelay; + currentFrameDelay = decoder->getFrameDelay_ms(); + long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate + currentFrameDelay -= tooSlowBy; + lastFrameDisplayTime = millis(); + } + return true; +} + +void endImagePlayback() { + if (file) file.close(); + delete decoder; + gifDecodeFailed = false; + activeSeg = nullptr; + lastFilename[0] = '\0'; +} + +#endif \ No newline at end of file diff --git a/wled00/wled.h b/wled00/wled.h index eac4cbc4d..e22603df3 100755 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -81,6 +81,8 @@ #define WIFI_MODE_AP WIFI_AP #include #endif + #undef WLED_DISABLE_GIF + #define WLED_DISABLE_GIF #else // ESP32 #include // ensure we have the correct "Serial" on new MCUs (depends on ARDUINO_USB_MODE and ARDUINO_USB_CDC_ON_BOOT) #include