Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added function to generate random palette based on harmonic color theory #3729

Merged
merged 11 commits into from
Feb 6, 2024
3 changes: 2 additions & 1 deletion wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@ typedef struct Segment {
// perhaps this should be per segment, not static
static CRGBPalette16 _randomPalette; // actual random palette
static CRGBPalette16 _newRandomPalette; // target random palette
static unsigned long _lastPaletteChange; // last random palette change time in millis()
static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000
static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF
#ifndef WLED_DISABLE_MODE_BLEND
static bool _modeBlend; // mode/effect blending semaphore
#endif
Expand Down
31 changes: 18 additions & 13 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for t
uint16_t Segment::maxWidth = DEFAULT_LED_COUNT;
uint16_t Segment::maxHeight = 1;

CRGBPalette16 Segment::_randomPalette = CRGBPalette16(DEFAULT_COLOR);
CRGBPalette16 Segment::_newRandomPalette = CRGBPalette16(DEFAULT_COLOR);
unsigned long Segment::_lastPaletteChange = 0; // perhaps it should be per segment
CRGBPalette16 Segment::_randomPalette = generateRandomPalette(_randomPalette);
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(_randomPalette);
uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment
uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only)

#ifndef WLED_DISABLE_MODE_BLEND
bool Segment::_modeBlend = false;
Expand Down Expand Up @@ -220,16 +221,11 @@ CRGBPalette16 IRAM_ATTR &Segment::loadPalette(CRGBPalette16 &targetPalette, uint
switch (pal) {
case 0: //default palette. Exceptions for specific effects above
targetPalette = PartyColors_p; break;
case 1: {//periodically replace palette with a random one
unsigned long timeSinceLastChange = millis() - _lastPaletteChange;
if (timeSinceLastChange > randomPaletteChangeTime * 1000U) {
_randomPalette = _newRandomPalette;
_newRandomPalette = CRGBPalette16(
CHSV(random8(), random8(160, 255), random8(128, 255)),
CHSV(random8(), random8(160, 255), random8(128, 255)),
CHSV(random8(), random8(160, 255), random8(128, 255)),
CHSV(random8(), random8(160, 255), random8(128, 255)));
_lastPaletteChange = millis();
case 1: {//periodically replace palette with a random one
if ((millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) {
_newRandomPalette = generateRandomPalette(_randomPalette);
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
_lastPaletteChange = millis()/1000U;
_lastPaletteBlend = (uint16_t)(millis()&0xFFFF)-512; //starts blending immediately
handleRandomPalette(); // do a 1st pass of blend
}
targetPalette = _randomPalette;
Expand Down Expand Up @@ -466,6 +462,15 @@ CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, u
void Segment::handleRandomPalette() {
// just do a blend; if the palettes are identical it will just compare 48 bytes (same as _randomPalette == _newRandomPalette)
// this will slowly blend _newRandomPalette into _randomPalette every 15ms or 8ms (depending on MIN_SHOW_DELAY)
// if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazoncek I was referring to this part (modifying transitions/blending)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. This refers to "respect transition time" while blending new random palette into old/displayable random palette.

My (or original) implementation relied on periodic (with constant period) calls to handleRandomPalette() to do the blend, (due to bug) this happened rather quickly (and not consistent between different ESPs)

The bug has been mitigated and the blend was consistent but still dependent on FPS. This change (although I'm not very fond of it) somehow mitigates that.
Reality is that it would need a separate timer and be independently controllable.

Copy link
Collaborator

@softhack007 softhack007 Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining 👍
Indeed transition/blending time should - in best case - not depend on framerates, but use millis() or other timers so it always looks smooth, and same time will be needed no matter how high or low users set their "target fps".

As its "your" code, you're the best person to know if a second PR is needed or not.

Copy link
Collaborator

@softhack007 softhack007 Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazoncek another thought:
We could measure "time elapsed" (in millis) since the last blend step. then calculate how many steps correspond to elapsed time (using 40 FPS as the baseline for changes). Put the blending into a loop. If steps needed > 0, perform several blends at once. OFC the blending code must still run at a high rate so it won't show visual stuttering.
This is similar to how audioreactive mitigates "hickups" in userloop activity, in order to stabilize audio filtering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softhack007 I already added in something like that but I think it needs to be polished a little more. It is not a question of coding it but more a question of how it should behave.
My opinion is, that the user should be given the option to set the blend speed, which I added in by tying it to 'Transition Speed' but IMHO this should be a separate value. Next question is then, does this blend speed control random palette only or is it also used for normal palette blending or will that still depend on transition time. Adding too many options is also bad for usability, it should be somehow logical which value controls what. Right now (without my changes but with @blazoncek fix for random blend) the Transition Time (which also controls palette blending) can be set to 5000ms for example, but random palettes will still blend at default speed, which is ~150 frames. To me as a user this is quite inconsistent.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are discussing it then let me try to explain "Transition Time" as far as blending palettes go. "Transition Time" is the time taken to switch (smoothly without visible steps) from one palette to the other. Random palette is exactly that - a single randomly generated palette. A single palette which is constantly changing so no transition is triggered when random change happen. If you want to consider this random change to be considered as a trigger for "Transition Time" then the approach taken above works until transition time is shorter than minimum time needed for full blend of two palettes (255 iterations) this is the inconsistency I am not fond of. I am not saying it will not work or that it may impact users much, I just want for things to be logical.

User can specify the amount of time between random changes (between 1s and 65535s), we could reuse that time to determine the time needed to transition from one random palette to the other without introducing yet another parameter or rely on "Transition Time". The connection can be linear, logarithmic or some other non-linear function. I.e. if a user specifies random palette time of 30s then the actual transition could take 1/3rd of time (or 10 seconds). This correlation will ensure no conflicts arise from improper user selection. I am open to suggestions regarding this approach.

In any case I'm ok with proposed solution taking strip.getTransition() into account.

Copy link
Collaborator Author

@DedeHai DedeHai Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree, to me the current solution (without this PR) with frame based random palette blends is inconsistent as well. If I set transitions to a slow 10s then change between palettes I get a smooth transition. If I switch to random palette, suddenly transitions are much faster.
I just try to imagine differen use cases and what a user would expect. In my case, I use it for 2D matrix displays and ambient lighting. For the display case, a frame based 'static' change would be fine. For ambient not so much (as I mentioned before), here I would want slow transitions or at least have some control over it.
Then I see lots of people using it for quite flashy displays, mostly sound reactive stuff. I think here a fast transition would be expected if transition time is set low, even if the random change is set to say 5 minutes, so tying the random transition time to the change interval may be a bad idea. These are the scenarios I was thinking about.
There is no 'one fits all' solution I think without adding another config parameter.
The currently proposed solution in this PR may be the best compromise. Some people may not like it, some may find it much better, most probably don't even care or notice ;)
The most consistent approach would be to adjust the random palette blend so it (approximately) fits the transition time, even if that is set lower than the current frame based transition. I could do that by adjusting the amount of blending done in 'handleRandomPalette' but keep the approach to just call that function once every frame.

if(strip.paletteFade)
{
if((millis()&0xFFFF) - _lastPaletteBlend < strip.getTransition()>>7) //assumes that 128 updates are needed to blend a palette, so shift by 7 (can be more, can be less)
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
{
return; //not time to fade yet
}
_lastPaletteBlend = millis();
}
nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48);
}

Expand Down
107 changes: 107 additions & 0 deletions wled00/colors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,113 @@ void setRandomColor(byte* rgb)
colorHStoRGB(lastRandomIndex*256,255,rgb);
}

/*
*generates a random palette based on color theory
*/

CRGBPalette16 generateRandomPalette(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
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
palettecolors[keepcolorposition].hue += random8(20)-10; // +/- 10 randomness
//generate 4 saturation and brightness value numbers
//only one saturation is allowed to be below 200 creating mostly vibrant colors
//only one brightness value number is allowed below 200, creating mostly bright palettes

for (int i = 0; i<3; i++) { //generate three high values
palettecolors[i].saturation = random8(180,255);
palettecolors[i].value = random8(180,255);
}
//allow one to be lower
palettecolors[3].saturation = random8(80,255);
palettecolors[3].value = random8(50,255);

//shuffle the arrays using Fisher-Yates algorithm
for (int i = 3; i > 0; i--) {
uint8_t j = random8(0, i + 1);
//swap [i] and [j]
uint8_t temp = palettecolors[i].saturation;
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
palettecolors[i].saturation = palettecolors[j].saturation;
palettecolors[j].saturation = temp;
}

for (int i = 3; i > 0; i--) {
uint8_t j = random8(0, i + 1);
//swap [i] and [j]
uint8_t temp = palettecolors[i].value;
palettecolors[i].value = palettecolors[j].value;
palettecolors[j].value = temp;
}

//now generate three new hues based off of the hue of the chosen current color
uint8_t basehue = palettecolors[keepcolorposition].hue;
uint8_t harmonics[3]; //hues that are harmonic but still a little random
uint8_t type = random8(5); //choose a harmony type

switch (type) {
case 0: // analogous
harmonics[0] = basehue + random8(30, 50);
harmonics[1] = basehue + random8(10, 30);
harmonics[2] = basehue - random8(10, 30);
break;

case 1: // triadic
harmonics[0] = basehue + 110 + random8(20);
harmonics[1] = basehue + 230 + random8(20);
harmonics[2] = basehue + random8(30)-15;
break;

case 2: // split-complementary
harmonics[0] = basehue + 140 + random8(20);
harmonics[1] = basehue + 200 + random8(20);
harmonics[2] = basehue + random8(30)-15;
break;

case 3: // tetradic
harmonics[0] = basehue + 80 + random8(20);
harmonics[1] = basehue + 170 + random8(20);
harmonics[2] = basehue + random8(30)-15;
break;

case 4: // square
harmonics[0] = basehue + 80 + random8(20);
harmonics[1] = basehue + 170 + random8(20);
harmonics[2] = basehue + 260 + random8(20);
break;
}

//shuffle the hues:
for (int i = 2; i > 0; i--) {
uint8_t j = random8(0, i + 1);
//swap [i] and [j]
uint8_t temp = harmonics[i];
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
harmonics[i] = harmonics[j];
harmonics[j] = temp;
}

//now set the hues
int j=0;
for (int i = 0; i<4; i++) {
if(i==keepcolorposition) continue; //skip the base color
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
palettecolors[i].hue = harmonics[j];
j++;
}

//apply gamma correction
CRGB RGBpalettecolors[4];
for (int i = 0; i<4; i++) {
RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB
RGBpalettecolors[i] = gamma32((uint32_t)RGBpalettecolors[i]);
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
}

return CRGBPalette16( RGBpalettecolors[0],
RGBpalettecolors[1],
RGBpalettecolors[2],
RGBpalettecolors[3]);

}

void colorHStoRGB(uint16_t hue, byte sat, byte* rgb) //hue, sat to rgb
{
float h = ((float)hue)/65535.0f;
Expand Down
1 change: 1 addition & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class NeoGammaWLEDMethod {
uint32_t color_blend(uint32_t,uint32_t,uint16_t,bool b16=false);
uint32_t color_add(uint32_t,uint32_t, bool fast=false);
uint32_t color_fade(uint32_t c1, uint8_t amount, bool video=false);
CRGBPalette16 generateRandomPalette(CRGBPalette16 &basepalette);
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 colorKtoRGB(uint16_t kelvin, byte* rgb);
Expand Down
3 changes: 3 additions & 0 deletions wled00/wled.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ using PSRAMDynamicJsonDocument = BasicJsonDocument<PSRAM_Allocator>;
#define PSRAMDynamicJsonDocument DynamicJsonDocument
#endif

#define FASTLED_INTERNAL //remove annoying pragma messages
#define USE_GET_MILLISECOND_TIMER
#include "FastLED.h"
#include "const.h"
DedeHai marked this conversation as resolved.
Show resolved Hide resolved
#include "fcn_declare.h"
#include "NodeStruct.h"
Expand Down