Smoothing / Interpolation / Precision Draw of LEDS

Hello chaps and chapettes!
First time poster, I hope I get this right.

I am attempting to build a small LED desk light.
Within said desk light I am using:

  • Arduino Nano
  • 5V/10A PSU
  • 70x WS2812B LEDs
  • The FastLED library.
  • An assortment of various resistors, pots and caps.

I've got some basic code set up, which, mind you, is basically a beginners amalgamation of multiple FastLED examples, youtube tutorials, reddit threads, naïve optimism and metaphorical duct tape.

The code is working. The electronics and the prototype LED light is working. It is doing what I am asking it to do.

The problem:
The discrete step nature of (basic) LED-array animation is quite underwhelming to look at.

If I want slower animations, they will instead look choppy in the current code.
I can increase the animation speed to make it flow a bit better, but I don't want it moving that fast.

I've tried looking at existing code for Precision Draw from Dave's Garage EP09 (I read it was not allowed to share code that I have not written myself). After hours of playing around with it I could not modify it so that it suited my needs.

The gif below is the version with slower refresh rate (80ms). This gives me a slower animation, but it also looks very laggy in real life.
Slow_LEDLIGHT

The second gif below has a reasonable refresh rate (20ms). This gives smoother animation, but it's quite too fast.
Fast_LEDLIGHT

The question:
How can I modify my code so that the animation speed can be slowed down without causing the discrete steps to be noticeable?

I am using the following code, it has been trimmed so that the vital functions are there. It works.

// #########################################################################################################

#include <FastLED.h>

#define LED_PIN 2
#define POT_PIN_BRIGHTNESS A3
#define NUM_LEDS 70
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define MAX_MILLIAMPS 500

CRGB leds[NUM_LEDS];

uint8_t brightnessAverage;
uint8_t brightnessAverage_map;
uint8_t startIndex = 0;
uint8_t potBrightness;


CRGBPalette16 currentPalette;
CRGBPalette16 targetPalette;

// #########################################################################################################


void setup() {
  delay(1500);                                                                                     // power-up safety delay.
  Serial.begin(9600);                                                                              // Initialize serial coms @ 9600 baud.
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);  // define led-strip.
  FastLED.setMaxPowerInVoltsAndMilliamps(5, MAX_MILLIAMPS);                                        // Upper limits for current draw (when only connected to usb)
  FastLED.setBrightness(50);                                                                       // initial brightness, not needed due to potBrightness.
  FastLED.clear();                                                                                 // clear the fastled data-line, I think.

  currentPalette = CreateRandomHuePalette();  // Picks a randomized new starting palette.
  targetPalette = currentPalette;             // Sets target palette to the starting palette so that initial nblend-function doesn't go crazy.
}

// #########################################################################################################

void loop() {

  EVERY_N_MILLISECONDS(20) {  // (ANIMATION SPEED / REFRESH RATE)

    startIndex++;                                                                             // Increments the palette index, causes it to move.
    potBrightness = BrightnessFromPot();                                                      // Fetches current brightness value from pot
    fill_palette(leds, NUM_LEDS, startIndex, 4, currentPalette, potBrightness, LINEARBLEND);  // Fills LED-data line with color palette values.
    nblendPaletteTowardPalette(currentPalette, targetPalette, 8);                             // Blends the current palette with the new target palette, causing a clean cross fade.

    FastLED.show();  // Push out rgb data in the LED-array (leds[]) to the physical LEDs.
  }

  EVERY_N_SECONDS(15) {  // Every 15 seconds, do
    targetPalette = CreateRandomHuePalette();
  }
}

// ###########################################################################################################
// This is probably far from the most efficient way of doing this. I am not a clever man, however.
// Basically, create a 16 color palette. Randomise a hue.
// Set an additional hue to the color wheel opposite of the first hue (sort of).
// Add them to a specific [x,y] in the 4x4 color palette array.
// Offset the hues a specific amount for a more interesting effect.

CRGBPalette16 CreateRandomHuePalette() {
  CRGBPalette16 randPalette;
  uint8_t randomHue1 = random8();
  uint8_t randomHue2 = randomHue1 - 96;
  uint8_t counter = 0;

  for (int i = 0; i < 13; i += 4) {

    if (counter == 0) {
      randPalette[i] = CRGB::Black;
      randPalette[i + 1] = CRGB::Black;
      randPalette[i + 2] = CHSV(randomHue1, 255, 255);
      randPalette[i + 3] = CHSV(randomHue2 + 10, 255, 255);
    }

    if (counter == 4) {
      randPalette[i] = CRGB::Black;
      randPalette[i + 1] = CRGB::Black;
      randPalette[i + 2] = CHSV(randomHue1 + 3, 255, 255);
      randPalette[i + 3] = CHSV(randomHue2 + 7, 255, 255);
    }

    if (counter == 8) {
      randPalette[i] = CRGB::Black;
      randPalette[i + 1] = CRGB::Black;
      randPalette[i + 2] = CHSV(randomHue1 + 7, 255, 255);
      randPalette[i + 3] = CHSV(randomHue2 + 3, 255, 255);
    }

    if (counter == 12) {
      randPalette[i] = CRGB::Black;
      randPalette[i + 1] = CRGB::Black;
      randPalette[i + 2] = CHSV(randomHue1 + 10, 255, 255);
      randPalette[i + 3] = CHSV(randomHue2, 255, 255);
    }
    counter = counter + 4;
  }
  return randPalette;
}

// ###################################### - READ FROM POTENTIOMETERS - #######################################

uint8_t BrightnessFromPot() {
  uint16_t potRead_b = analogRead(POT_PIN_BRIGHTNESS);    // Reads analog value from potentiometer.
  uint8_t brightness = map(potRead_b, 0, 1023, 60, 255);  // Maps the 16-bit analog value to an 8-bit value.

  uint8_t temp_bright_a = (1.0 / 10.0) * brightness;                  // Half of running average to stabilize jitter in the analog value.
  uint8_t temp_bright_b = ((10.0 - 1.0) / 10.0) * brightnessAverage;  // Other half
  brightnessAverage = temp_bright_a + temp_bright_b;                  // Averaged brightness level over N=10 samples. (Thank you /u/stockvu!)

  brightnessAverage_map = map(brightnessAverage, 60, 241, 65, 255);  // Averaged values only go from 60 - 241 instead of 0 - 255, this re-maps them to 65-255
  return brightnessAverage_map;
}

// ###########################################################################################################

Thank you for your time.

And If you think that my code is a dumpster fire, I am always interested in feedback - let me know how I can do better!

Change the animation speed / refresh rate.

The final parameter is this function controls how fast the colours change. Try reducing that

nblendPaletteTowardPalette(currentPalette, targetPalette, 8);                             // Blends the current palette with the new target palette, causing a clean cross fade.

but keep the update rate at 20ms.

Comment-these out until you need the pause before start and the serial output.

It might not be causing a problem right now, but this function uses floating point maths.

uint8_t BrightnessFromPot() {
  uint16_t potRead_b = analogRead(POT_PIN_BRIGHTNESS);    // Reads analog value from potentiometer.
  uint8_t brightness = map(potRead_b, 0, 1023, 60, 255);  // Maps the 16-bit analog value to an 8-bit value.

  uint8_t temp_bright_a = (1.0 / 10.0) * brightness;                  // Half of running average to stabilize jitter in the analog value.
  uint8_t temp_bright_b = ((10.0 - 1.0) / 10.0) * brightnessAverage;  // Other half
  brightnessAverage = temp_bright_a + temp_bright_b;                  // Averaged brightness level over N=10 samples. (Thank you /u/stockvu!)

  brightnessAverage_map = map(brightnessAverage, 60, 241, 65, 255);  // Averaged values only go from 60 - 241 instead of 0 - 255, this re-maps them to 65-255
  return brightnessAverage_map;
}

which is slow on almost all Arduino because it is done in software rather than in hardware, like it is on your PC/laptop's CPU.

Using floating point maths also brings in more code libraries, increasing the size of your code. Again, that's not really a problem at the moment.

Also, this function uses map() twice. The first time is to reduce a 16-bit integer value down to an 8-bit. Then it calculates a moving average before using map() a second time to get the required final range of values. This can also be simplified and made more efficient.

It would be easy to remove the use of floating point maths:

uint8_t BrightnessFromPot() {
  uint16_t potRead_b = analogRead(POT_PIN_BRIGHTNESS);    // Reads analog value from potentiometer.

  brightnessAverage = (potRead_b + 9 * brightnessAverage) / 10;                  // Averaged brightness level over N=10 samples. (Thank you /u/stockvu!)

  brightnessAverage_map = map(brightnessAverage, 0, 1023, 65, 255); 
  return brightnessAverage_map;
}

Does the above work ok?

I think I might have explained my issue poorly.

If I change the refresh rate to a larger value, say 80ms - the animation does slow down.
However, the distinct movements in the animation become very visible. It looks laggy and not very appealing.

It does not show up very well on camera, but is very noticeable IRL.

What I think I need is a float variable acting as pointer for where interpolation / smoothing should happen. But with palettes I would have no idea how to go about implementing it.


See above as well. The issue at hand isn't how fast / slow the colors are blending between palettes. More how choppy things look when slowing down animation speeds.

I would like high refresh rates, but a slow animation that is decoupled from refresh rate.


I had to change brightnessAverage from a uint8_t to a uint16_t, then it worked like a charm! Thank you for the pointer!

I think you missed my point. Things will look choppy if you slow down the refresh rate. So keep it high and slow down the blending rate. This should slow down the speed at which the colours change without making it choppy.

I only use the nblendPaletteTowardPalette when I am changing from one colored palette to another.

The issue still exists if I only use one palette and remove nblendPaletteTowardPalette.

The choppy nature of the animation stems from the following part of the code:

fill_palette(leds, NUM_LEDS, startIndex++, 4, currentPalette, potBrightness, LINEARBLEND);

The int startIndex increments each loop which "shifts" the entire palette.

If I run the code every 20ms it increments quickly == smooth, but very fast visible movement.
If I run the code every 250ms it increments slowly == laggy, choppy, discrete stepped.


I've been trying the following approach instead, but it causes flickering:

uint16_t fadeTime = 1000;

blendAmount = map(millis() % fadeTime, 0, fadeTime, 0, 255);

EVERY_N_MILLISECONDS(fadeTime) {  // 1000ms

        startIndex++;

        fill_palette(ledsNow, NUM_LEDS, startIndex, 4, palette, brightness, LINEARBLEND);
        fill_palette(ledsNext, NUM_LEDS, startIndex + 1, 4, palette, brightness, LINEARBLEND);
      }

EVERY_N_MILLISECONDS(20) {

      blend(ledsNow, ledsNext, leds, NUM_LEDS, blendAmount);
      FastLED.show();
}

This should in theory do the following:
Fill ledsNow with my palette.
Fill ledsNext with my palette, shifted 1 step.

Fill my output led array, leds, with the palette contained within ledsNow.
Then it blends it evenly between the palette shifted 1 step (ledsNext) over 1000ms.

From what I gathered it should basically fade one frame of the animation toward the next frame over the course of 1000ms.

But yeah no, this is very jittery and is also eating up my dynamic memory.

I hope my thoughts here are clear, sorry if not; In small-scale Neopixel lighting, I find that flickering can be lessened/eliminated by writing changing data to the buffer after the FastLED.show() leaves "old" data in the buffer, and in the next loop, the new data which was written to the buffer (along with/over the old data) to be "shown" at the next FastLED.show().

You've only got 70 NUM_LEDS divided among 10 balloon things to get about 7 LEDs per balloon. If it steps too fast, you could slow it down if you could take same-sized steps at a slower rate and get chunkier movement, or take smaller steps at the same speed and get a slower, smoother speed. Maybe lower the "4" in incIndex?

incIndex -- how much to increment the palette color index per LED