diff --git a/wled00/FX.cpp b/wled00/FX.cpp index ad843f0f95..6700c02cdb 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -82,6 +82,7 @@ static um_data_t* getAudioData() { return um_data; } + // effect functions /* @@ -93,6 +94,38 @@ uint16_t mode_static(void) { } static const char _data_FX_MODE_STATIC[] PROGMEM = "Solid"; +/* + * Copy a segment and perform (optional) color adjustments + */ +uint16_t mode_copy_segment(void) { + uint32_t sourceid = SEGMENT.custom3; + if (sourceid >= strip.getSegmentsNum() || sourceid == strip.getCurrSegmentId()) { // invalid source + SEGMENT.fadeToBlackBy(5); // fade out, clears pixels and allows overlapping segments + return FRAMETIME; + } + Segment sourcesegment = strip.getSegment(sourceid); + if (sourcesegment.isActive()) { + uint32_t sourcecolor; + if(!sourcesegment.is2D()) { // 1D source, source can be expanded into 2D + uint32_t cl; // length to copy + for (unsigned i = 0; i < SEGMENT.virtualLength(); i++) { + sourcecolor = strip.getRenderedPixelXY(sourceid, i); + SEGMENT.setPixelColor(i, adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2)); + } + } else { // 2D source, note: 2D to 1D just copies the first row (or first column if 'Switch axis' is checked in FX) + for (unsigned y = 0; y < SEGMENT.virtualHeight(); y++) { + for (unsigned x = 0; x < SEGMENT.virtualWidth(); x++) { + if(SEGMENT.check2) sourcecolor = strip.getRenderedPixelXY(sourceid, y, x); // flip axis (for 2D -> 1D, in 2D Segments this does the same as 'Transpose') + else sourcecolor = strip.getRenderedPixelXY(sourceid, x, y); + SEGMENT.setPixelColorXY(x, y, adjust_color(sourcecolor, SEGMENT.intensity, SEGMENT.custom1, SEGMENT.custom2)); + } + } + } + } + return FRAMETIME; +} +static const char _data_FX_MODE_COPY[] PROGMEM = "Copy Segment@,Color shift,Lighten,Brighten,ID,,Switch axis(2D);;;1;ix=0,c1=0,c2=0,c3=0,o2=0"; + /* * Blink/strobe function @@ -7830,6 +7863,7 @@ void WS2812FX::setupEffectData() { _modeData.push_back(_data_RESERVED); } // now replace all pre-allocated effects + addEffect(FX_MODE_COPY, &mode_copy_segment, _data_FX_MODE_COPY); // --- 1D non-audio effects --- addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); diff --git a/wled00/FX.h b/wled00/FX.h index 3c28274d60..c96b1342de 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -317,8 +317,8 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_COPY 187 +#define MODE_COUNT 188 typedef enum mapping1D2D { M12_Pixels = 0, @@ -847,7 +847,8 @@ class WS2812FX { // 96 bytes uint32_t now, timebase, - getPixelColor(uint16_t) const; + getPixelColor(uint16_t) const, + getRenderedPixelXY(uint8_t segid, unsigned x, unsigned y = 0) const; inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 737d5f7a47..74e57101c0 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1422,6 +1422,18 @@ uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) const { return BusManager::getPixelColor(i); } +/* + * Read rendered pixel back (following mirror/reverse/transpose but ignoring grouping) + */ +uint32_t WS2812FX::getRenderedPixelXY(uint8_t segid, unsigned x, unsigned y) const { + // For every group-length pixels, add spacing + x *= _segments[segid].groupLength(); // expand to physical pixels + y *= _segments[segid].groupLength(); // expand to physical pixels + if (x >= _segments[segid].width() || y >= _segments[segid].height()) return 0; // fill out of range pixels with black + uint32_t offset = _segments[segid].is2D() ? 0 : _segments[segid].offset; //offset in 2D segments is undefined, set to zero + return strip.getPixelColorXY(_segments[segid].start + offset + x, _segments[segid].startY + y); +} + void WS2812FX::show() { // avoid race condition, capture _callback value show_callback callback = _callback; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 478a0a277d..f43a0f9fb3 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -91,6 +91,23 @@ uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) return scaledcolor; } +/* + * color adjustment in HSV color space (converts RGB to HSV and back), color conversions are not 100% accurate! + shifts hue, increase brightness, decreases saturation (if not black) + note: inputs are 32bit to speed up the function, useful input value ranges are 0-255 + */ +uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten) { + if(rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change + CHSV32 hsv; + rgb2hsv(rgb, hsv); //convert to HSV + hsv.h += (hueShift << 8); // shift hue (hue is 16 bits) + hsv.s = max((int32_t)0, (int32_t)hsv.s - (int32_t)lighten); // desaturate + hsv.v = min((uint32_t)255, (uint32_t)hsv.v + brighten); // increase brightness + uint32_t rgb_adjusted; + hsv2rgb(hsv, rgb_adjusted); // convert back to RGB TODO: make this into 16 bit conversion + return rgb_adjusted; +} + void setRandomColor(byte* rgb) { lastRandomIndex = get_random_wheel_index(lastRandomIndex); @@ -105,7 +122,7 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) { CHSV palettecolors[4]; //array of colors for the new palette uint8_t keepcolorposition = random8(4); //color position of current random palette to keep - palettecolors[keepcolorposition] = rgb2hsv_approximate(basepalette.entries[keepcolorposition*5]); //read one of the base colors of the current palette + palettecolors[keepcolorposition] = rgb2hsv(basepalette.entries[keepcolorposition*5]); //read one of the base colors of the current palette palettecolors[keepcolorposition].hue += random8(10)-5; // +/- 5 randomness of base color //generate 4 saturation and brightness value numbers //only one saturation is allowed to be below 200 creating mostly vibrant colors @@ -206,28 +223,66 @@ CRGBPalette16 generateRandomPalette() //generate fully random palette CHSV(random8(), random8(160, 255), random8(128, 255))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) { - float h = ((float)hue)/10922.5f; // hue*6/65535 - float s = ((float)sat)/255.0f; - int i = int(h); - float f = h - i; - int p = int(255.0f * (1.0f-s)); - int q = int(255.0f * (1.0f-s*f)); - int t = int(255.0f * (1.0f-s*(1.0f-f))); - p = constrain(p, 0, 255); - q = constrain(q, 0, 255); - t = constrain(t, 0, 255); - switch (i%6) { - case 0: rgb[0]=255,rgb[1]=t, rgb[2]=p; break; - case 1: rgb[0]=q, rgb[1]=255,rgb[2]=p; break; - case 2: rgb[0]=p, rgb[1]=255,rgb[2]=t; break; - case 3: rgb[0]=p, rgb[1]=q, rgb[2]=255;break; - case 4: rgb[0]=t, rgb[1]=p, rgb[2]=255;break; - case 5: rgb[0]=255,rgb[1]=p, rgb[2]=q; break; + unsigned int remainder, region, p, q, t; + unsigned int h = hsv.h; + unsigned int s = hsv.s; + unsigned int v = hsv.v; + if (s == 0) { + rgb = v << 16 | v << 8 | v; + return; + } + region = h / 10923; // 65536 / 6 = 10923 + remainder = (h - (region * 10923)) * 6; + p = (v * (256 - s)) >> 8; + q = (v * (255 - ((s * remainder) >> 16))) >> 8; + t = (v * (255 - ((s * (65535 - remainder)) >> 16))) >> 8; + switch (region) { + case 0: + rgb = v << 16 | t << 8 | p; break; + case 1: + rgb = q << 16 | v << 8 | p; break; + case 2: + rgb = p << 16 | v << 8 | t; break; + case 3: + rgb = p << 16 | q << 8 | v; break; + case 4: + rgb = t << 16 | p << 8 | v; break; + default: + rgb = v << 16 | p << 8 | q; break; } } +void rgb2hsv(const uint32_t rgb, CHSV32& hsv) // convert RGB to HSV (16bit hue), much more accurate and faster than fastled version +{ + hsv.raw = 0; + int32_t r = (rgb>>16)&0xFF; + int32_t g = (rgb>>8)&0xFF; + int32_t b = rgb&0xFF; + int32_t minval, maxval, delta; + minval = min(r, g); + minval = min(minval, b); + maxval = max(r, g); + maxval = max(maxval, b); + if (maxval == 0) return; // black + hsv.v = maxval; + delta = maxval - minval; + hsv.s = (255 * delta) / maxval; + if (hsv.s == 0) return; // gray value + if (maxval == r) hsv.h = (10923 * (g - b)) / delta; + else if (maxval == g) hsv.h = 21845 + (10923 * (b - r)) / delta; + else hsv.h = 43690 + (10923 * (r - g)) / delta; +} + +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) { //hue, sat to rgb + uint32_t crgb; + hsv2rgb(CHSV32(hue, sat, 255), crgb); + rgb[0] = byte((crgb) >> 16); + rgb[1] = byte((crgb) >> 8); + rgb[2] = byte(crgb); +} + //get RGB values from color temperature in K (https://tannerhelland.com/2012/09/18/convert-temperature-rgb-algorithm-code.html) void colorKtoRGB(uint16_t kelvin, byte* rgb) //white spectrum to rgb, calc { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 8903d1f273..914433f946 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -66,6 +66,29 @@ typedef struct WiFiConfig { } wifi_config; //colors.cpp +struct CHSV32 { // 32bit HSV color with 16bit hue for more accurate conversions + union { + struct { + uint16_t h; // hue + uint8_t s; // saturation + uint8_t v; // value + }; + uint32_t raw; // 32bit access + }; + inline CHSV32() __attribute__((always_inline)) = default; // default constructor + + /// Allow construction from hue, saturation, and value + /// @param ih input hue + /// @param is input saturation + /// @param iv input value + inline CHSV32(uint16_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 16bit h, s, v + : h(ih), s(is), v(iv) {} + inline CHSV32(uint8_t ih, uint8_t is, uint8_t iv) __attribute__((always_inline)) // constructor from 8bit h, s, v + : h((uint16_t)ih << 8), s(is), v(iv) {} + inline CHSV32(const CHSV& chsv) __attribute__((always_inline)) // constructor from CHSV + : h((uint16_t)chsv.h << 8), s(chsv.s), v(chsv.v) {} + inline operator CHSV() const { return CHSV((uint8_t)(h >> 8), s, v); } // typecast to CHSV +}; // similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) class NeoGammaWLEDMethod { public: @@ -81,10 +104,14 @@ class NeoGammaWLEDMethod { [[gnu::hot]] uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false); [[gnu::hot]] uint32_t color_add(uint32_t,uint32_t, bool fast=false); [[gnu::hot]] uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false); +uint32_t adjust_color(uint32_t rgb, uint32_t hueShift, uint32_t lighten, uint32_t brighten); CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } -void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); //hue, sat to rgb +void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); +void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); +void rgb2hsv(const uint32_t rgb, CHSV32& hsv); +inline CHSV rgb2hsv(const CRGB c) { CHSV32 hsv; rgb2hsv((uint32_t((byte(c.r) << 16) | (byte(c.g) << 8) | (byte(c.b)))), hsv); return CHSV(hsv); } // CRGB to hsv void colorKtoRGB(uint16_t kelvin, byte* rgb); void colorCTtoRGB(uint16_t mired, byte* rgb); //white spectrum to rgb void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO diff --git a/wled00/ir.cpp b/wled00/ir.cpp index e4541cd909..f094d3b874 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -129,7 +129,7 @@ static void changeEffectSpeed(int8_t amount) } else { // if Effect == "solid Color", change the hue of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t)prim_hsv.h + amount; if (new_val > 255) new_val -= 255; // roll-over if bigger than 255 if (new_val < 0) new_val += 255; // roll-over if smaller than 0 @@ -173,7 +173,7 @@ static void changeEffectIntensity(int8_t amount) } else { // if Effect == "solid Color", change the saturation of the primary color Segment& sseg = irApplyToAllSelected ? strip.getFirstSelectedSeg() : strip.getMainSegment(); CRGB fastled_col = CRGB(sseg.colors[0]); - CHSV prim_hsv = rgb2hsv_approximate(fastled_col); + CHSV prim_hsv = rgb2hsv(fastled_col); int16_t new_val = (int16_t) prim_hsv.s + amount; prim_hsv.s = (byte)constrain(new_val,0,255); // constrain to 0-255 hsv2rgb_rainbow(prim_hsv, fastled_col);