How to modify existing patterns to use Palette functions in v3.30

I’m back to building, and excited by the new features in recent releases! I’m trying to finish a v2 of a project (a new wearable coat) for an event in two weeks – I have it working with out-of-the-box palettes and looking great (I’ll do a full writeup after) but I’m not sure I understand how the new palette functions work.

Specifically, I want to modify “sound - spectrum analyser 1D/2D” (from the pattern library) to change from displaying the full rainbow to just a set of colors from a palette. Then, I want to automatically blend between that palette and a second (and third… etc) palette every, say, 30 seconds.

For the first part, I think it should be easy, but I’m just not seeing how to swap the below code to call a palette function.

For the second part, that’d use “nblendPaletteTowardPalette” in FastLED, but may just not be part of the implementation yet? No worries if so, consider it a feature request if no one has figured a simple solve or an ask for coding help – just the ability to switch active palettes over time would be nice, but blending them would be great.

(This is a generalizable problem, but this is the specific pattern I’m trying to do it with first – it should work with modifying a whole RANGE of existing patterns as well!).

Example of the way I want this to work, with psuedo-code and comments with “HELP!!!”) where I’m stuck follows. The example patterns I’m using are from an old FastLED project I did in 2019, and used the
great cpt-city palette resource, converted for FastLED with gammas (2.6, 2.2, 2.5) – bonus question: do I need to do that conversion with how PixelBlaze implements colors?

Thanks in advance!

var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]

var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]

setPalette(black_Blue_Magenta_White_gp)

/* HELP!!! set an array of palettes, using the two above as the examples, 
but it shouldn't matter how many palettes we define here. */

// ... rest of normal sound - spectrum analyser 1D/2D code here.

export function render2D(index, x, y) {
// most lines in this section are just the normal pattern's code for reference.

  xPixel = floor(x * width)  // Converts 0..1 'world units' x into pixel width
  yPixel = height - 1 - floor(y * height) // Invert so baseline is yPixel == 0

  // original code: h = hueT + x // Cycle the bar color through the rainbow. hsv() 'wraps' h."
  // HELP!!! using a palette function instead of the whole rainbow here. 

  s = 1
  v = fy[xPixel] > yPixel  // Fill bars from 0..fy[xPixel]
  
  // If this is a peak pixel, apply the peakHSV color
  if (peaks[xPixel] == yPixel) {
    h = peakHSV[0]; s = peakHSV[1]; v = peakHSV[2]

 // HELP!!! If time elapsed >30 seconds, blend palette to second palette. 
 // If blending is hard, at least swap to the second palette. 
  }
  
  hsv(h, s, v)
}
1 Like

Replying to myself to break this large code block out of the main question; I found the Pattern in the Library called “Crossfading” which I think does the nblendPaletteTowardPalette part, but brute forces that part of the question, and (since it’s three years old!) doesn’t using the new frameworks for palettes. It’s pasted below for reference, but is almost certainly way too complicated / too much overhead for me if it can’t be done much simplier using the newly released capabilities.

(I also still don’t understand what should be the easiest part – replacing the rainbow with a single palette call!)

/*
  This example demonstrates crossfading between three patterns.
  
  Demo on 8x8 matrix: https://youtu.be/tjCt1LdiZ5c
  
  This is accomplished by replacing all calls to hsv() with _hsv()
  and rgb() with _rgb(). The underscored functions store output
  in global variables r, g, and b. 
  
  blend() combines these RGB values using a simple weighted average.
  
  To combine patterns, first copy them together into one and de-conflict 
  any global variable names, like `v` or `t1`. Then rename each beforeRender() 
  and render2D() into anonymous functions in their respective arrays.
  
  For example, for the first pattern: 
    export function beforeRender(delta) {}
  becomes:
    beforeRenders[0] = function (delta) {}
  and
    export function render2D(index, x, y) {}
  becomes
    renderers[0] = (index, x, y) => {} // lambda form example

  Caveats
  
  render() in this pattern uses Pixelblaze's rgb() instead of hsv() out of 
  laziness, since the crossfader averages r, g, and b values. Note that 
  the special 24+5 bit HDR color rendering provided by hsv() is lost. If
  you implement an rgb2hsv() and check the math to make sure precision is
  maintained, you can have it back at the expense of framerate.
  
  Direct HSV blending seems to be computationally expensive by computing 
  quaternion SLerp, so we'll leave that for another day.
*/


modeCount = 3            // Total number of sub-patterns contained within this one
secondsPerMode = 5
xFadePct = 0.4           // Percentage of the time we spend in crossfades

beforeRenders = array(modeCount)
renderers = array(modeCount)

var mode, nextMode       // Integer index of the current and next sub-pattern 
var r, g, b              // Global temp variables
var pctIntoXFade         // Percentage we're into the current crossfade


export function beforeRender(delta) {
  modeTime = time(secondsPerMode * modeCount / 65.536) * modeCount
  mode = floor(modeTime)
  nextMode = (mode + 1) % modeCount

  // 0 when not crossfading; 0..0.999 when crossfading
  pctIntoXFade = max(((modeTime % 1) - (1 - xFadePct)) / xFadePct, 0)

  // Compute the right set of beforeRender()'s
  beforeRenders[mode](delta)
  if (pctIntoXFade > 0) beforeRenders[nextMode](delta)
}

export function render(index) {
  // A renderer is expected to either set globals r, g, and b or call _hsv() to do it. 
  renderers[mode](index)
  
  // If it's time to crossfade
  if (pctIntoXFade > 0) {
    r0 = r; g0 = g; b0 = b;    // Stash the first renderer's r, g, & b
    renderers[nextMode](index) // Compute the next pattern; also expected to set r, g, & b
    blend(r0, g0, b0, r, g, b)
  } else {
    rgb(r, g, b)
  }
}



// Blend computes the simple weighted average of R, G, and B component values
function blend(r1, g1, b1, r2, g2, b2) {
  r = r1 * (1 - pctIntoXFade) + r2 * pctIntoXFade
  g = g1 * (1 - pctIntoXFade) + g2 * pctIntoXFade
  b = b1 * (1 - pctIntoXFade) + b2 * pctIntoXFade
  rgb(r, g, b)
}

// Convert your individual patterns' calls to hsv() and rgb() to use these wrappers
// Instead of setting the pixel value, these store the result in global variables r, g, & b
// and render() will take care of setting them or blending two of them.
function _rgb(_r, _g, _b)   { r = _r; g = _g; b = _b }
function _hsv(_h, _s, _v)   { hsv2rgb(_h, _s, _v) }
function _hsv24(_h, _s, _v) { hsv2rgb(_h, _s, _v) }

// Converts HSV values to RGB values, storing the result in global variables r, g, & b
function hsv2rgb(_h, _s, _v) {
    if (_h < 0) _h = 1 - abs(_h) // wrap negative hue
    h = _h % 1
    s = clamp(_s, 0, 1) 
    v = clamp(_v, 0, 1) 
    i = floor(h * 6)
    f = h * 6 - i
    p = v * (1 - s)
    q = v * (1 - (s * f))
    t = v * (1 - (s * (1 - f)))

    if (i == 0) {
      r = v; g = t; b = p
    } else if (i == 1) {
      r = q; g = v; b = p
    } else if (i == 2) {
      r = p; g = v; b = t
    } else if (i == 3) {
      r = p; g = q; b = v
    } else if (i == 4) {
      r = t; g = p; b = v
    } else if (i == 5) {
      r = v; g = p; b = q
    } 
}



// Here's the code copied in and converted from other patterns

// Blue-purple shimmer
beforeRenders[0] = (delta) => { t1 = time(5 / 65.536) }

renderers[0] = (index) => {
  h = 0.5 + 0.333 * wave(1 / (30 * (index / pixelCount + 0.1 * sin((t1 + 0.5) * PI2) - 0.75)))
  _hsv(h, 1, 0.5 + wave(t1 / 4 * PI2)/2)
}

// Rainbow blocks
beforeRenders[1] = (delta) => { t10 = time(3 / 65.536) }

renderers[1] = (index) => {
  h = index/pixelCount + t10
  v = (index/pixelCount % (1/8)) >= 1/16
  _hsv(h, 1, v)
}

// Bouncing red pulse = Low budget KITT
beforeRenders[2] = (delta) => { t20 = time(3 / 65.536) }

renderers[2] = (index) => {
  r = 1 - clamp(abs(index - triangle(t20) * pixelCount) / 3, 0, 1)
  _rgb(r, 0, 0)
}

Below is a short pattern that should give you a little insight into what’s going on with palettes. It’s actually easier to explain with code.

Interpolating between palettes can be done, but far as I know, it’s not directly implemented on Pixelblaze.

A few key things to know:

  • all colors, both RGB and HSV on Pixelblaze are normalized to a 0.0 to 1.0 range. Never 0-255. Eventually everybody’s going to get there – it makes color calculations much easier and works without change on LEDs with greater than 8-bit color depth.
  • Pixelblaze palettes are arrays of [range, r,g,b] values, again in the range 0.0 to 1.0. The attached code shows one way of converting from online 0-255 tools.
  • to set the currently active palette, use setPalette() function.
  • instead of using the hsv() or rgb() function to set color, when you’re using a palette, you use the paint(value) function, where value is the 0…1 based position in the currently selected palette. Paint can also take an optional brightness as a second parameter.
// palette visualizer example
var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]
  
// normalize palette to 0.0 to 1.0 range  
arrayMutate(black_Blue_Magenta_White_gp,(v, i ,a) => v / 255);  

var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]
  
// normalize palette to 0.0 to 1.0 range    
arrayMutate(es_landscape_33_gp,(v, i ,a) => v / 255);

var palettes = [black_Blue_Magenta_White_gp,es_landscape_33_gp]
export var paletteIndex = 0;
export var timeAccumulator = 0.0

export function beforeRender(delta) {
  // get accumulated run time in seconds, wrapping every hour
 // to keep fixed-point precision in a "good" range.
  timeAccumulator = (timeAccumulator+delta/1000) % 3600;
  
  // switch palettes every five seconds, for test purposes
  if (timeAccumulator > 5) {
    paletteIndex = (paletteIndex + 1) % 2;
    timeAccumulator = 0;
  }
  
  // tell Pixelblaze which palette to use
  setPalette(palettes[paletteIndex])
  t1 = time(.1)
}

export function render(index) {
  pct = frac(t1 + index/pixelCount)
  paint(pct)
}
1 Like

Crazy awesome tip: the array you use with setPalette is LIVE, and can be modified as you see fit.

If the number of colors and positions of two or more palettes is the same, it’s fairly easy to mix/blend 2 source palettes into a third array that is used with setPalette.

Otherwise you might need to take the fastled approach and render them into something with fixed positions (like 16ths), then blend those.

Blockquote Crazy awesome tip: the array you use with setPalette is LIVE, and can be modified as you see fit.

@wizard, I was thinking exactly this when considering, “how would I fade between two palettes”.
Just stipulate that the size and position intervals of all your palettes must be the same, and
you can use mix() to generate a new palette at any blend interval.

It’d be helpful someday to be able to get the interpolated palette color outside of paint() too - something like getPaletteR(), getPaletteG(), getPaletteB() .

First off, thanks a ton @zranger1 – you’ve answered all of my questions well enough that with some fiddling I got the first question working!

I got your palette switcher code working perfectly, and implemented it on the Sinpulse 1D/2D/3D code by @Whitespace here as a proof of concept first. It works great, and that combined code would make a good addition to the pattern library (this is a pretty generically useful thing to solve) – @wizard I think it’d help people who don’t “grok” palettes a lot or who are coming from FastLED and get how it does Palettes. The helper function in this thread also enables all of the PaletteKnife magic helper bookmarklet to just work – PaletteKnife for FastLED can be ported with effectively zero changes to find and set palettes!

I hear what you’re both saying in terms of blending palettes, but I’m not sure the implementation is in-scope for me on this project unless either of you have time to write up some sample code – it’d really aid the implementation I’m doing, but I’m not quite sure I you well enough.

For an example – using the same project – why the blending would help me a lot, this linked below (8 second) video is using the concepts in this thread to modify the Honeycomb 2D pattern with two palettes; you can see the very abrupt cutover between them a couple of seconds in, which is certainly not ideal.

(Fun fact, this is 290 LEDs in one big fluffy coat).

Example PixelBlaze Palette Switching Coat

Love the coat, @ZacharyRD ! The way it diffuses the LEDs looks fantastic.

Just for academic completeness, here’s a short pattern that shows a way of blending between two arbitrary palettes. The palette sizes and gradient stops don’t have to match.

There’s a lot of per-pixel calculation involved-- not the fastest thing in the world, so depending on your target framerate, I wouldn’t recommend using it with much more than about 300 pixels.

var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]
  
// normalize palette to 0.0 to 1.0 range  
arrayMutate(black_Blue_Magenta_White_gp,(v, i ,a) => v / 255);  

var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]
  
// normalize palette to 0.0 to 1.0 range    
arrayMutate(es_landscape_33_gp,(v, i ,a) => v / 255);

// arrays to hold rgb interpolation results
var pixel1 = array(3);
var pixel2 = array(3);

// user space version of Pixelblaze's paint function. Stores
// interpolated rgb color in rgbArray
function paint2(v, rgbArray, pal) {
  var k,u,l;
  var rows = pal.length / 4;

  // find the top bounding palette row
  for (i = 0; i < rows;i++) {
    k = pal[i * 4];
    if (k >= v) break;
  }

  // fast path for special cases
  if ((i == 0) || (i >= rows) || (k == v)) {
    i = 4 * min(rows - 1, i);
    rgbArray[0] = pal[i+1];
    rgbArray[1] = pal[i+2];
    rgbArray[2] = pal[i+3];    
  }
  else {
    i = 4 * (i-1);
    l = pal[i]   // lower bound    
    u = pal[i+4]; // upper bound

    pct = 1 -(u - v) / (u-l);
    
    rgbArray[0] = mix(pal[i+1],pal[i+5],pct);
    rgbArray[1] = mix(pal[i+2],pal[i+6],pct);
    rgbArray[2] = mix(pal[i+3],pal[i+7],pct);    
  }
}

// interpolate colors within and between two palettes
function paletteMix(pal1, pal2, colorPct,palettePct) {
  paint2(colorPct,pixel1,pal1);
  paint2(colorPct,pixel2,pal2);  
  
  rgb(mix(pixel1[0],pixel2[0],palettePct),
      mix(pixel1[1],pixel2[1],palettePct),
      mix(pixel1[2],pixel2[2],palettePct)
   )
}

var palettes = [black_Blue_Magenta_White_gp,es_landscape_33_gp]
export var paletteIndex = 0;

export function beforeRender(delta) {
  t1 = time(0.1);
}

export function render(index) {
  pct = frac(t1 + index/pixelCount)
  paletteMix(palettes[0],palettes[1],pct,wave(t1));
}
1 Like

Thanks! This is clearly a huge advantage for both me and for everyone else trying to do this if I can get it to work… and I’m not seeing quite how you’re doing it and how to apply it, @zranger1 .

In the prior code, which I minorly adjusted to make more generic, and is working, we’re using a really simple code block with “beforeRender” –

  // switch palettes every 8 seconds
  if (timeAccumulator > 8) {
    paletteIndex = (paletteIndex + 1) % numPalettes;
    timeAccumulator = 0;
  }
  // tell Pixelblaze which palette to use
  setPalette(palettes[paletteIndex])

to change between palettes, and then to render them, I’m just using a simple:

  paint(h,v)

Given the code you provided, what do I actually do within the beforeRender to switch / blend between palettes, and then how do I use paint2 to call it?

You can see the entire pattern below in a long code block – it’s basically the honeycomb “stock” pattern, with a heavy dose of palette magic mixed in. It’s pretty well commented on what isn’t working right now.

The root of the challenge is just in the render2D function.

Thanks a lot – I think this is something that’ll end up useful to other people as well. (And if you’re over the problem, I’d understand that too – but I’m hoping it’s a pretty easy fix to explain or do)

/*
  Honeycomb 2D - with dynamic palettes
  This code takes the Honeycomb 2d code and makes it rotate between different 
  palettes. It SHOULD blend between them, but that code is inactive and currently
  it just hard-swaps between them instead. 

  This pattern is meant to be displayed on an LED matrix or other 2D surface
  defined in the Mapper tab, but also has 1D and 3D renderers defined.
  
  Output demo: https://youtu.be/u9z8_XGe684
  
  The mapper allows us to share patterns that work in 2D or 3D space without the
  pattern code being dependent on how the LEDs were wired or placed in space.
  That means these three installations could all render the same pattern after
  defining their specific LED placements in the mapper:
    
    1. A 8x8 matrix in a perfect grid, wired the common zigzag way
    2. Individual pixels on a strand mounted in a triangle hexagon grid
    3. Equal length strips wired as vertical columns on separate channels
         of the output expander board
  
  To get started quickly with matrices, there's an inexpensive 8x8 on the 
  Pixelblaze store. Load the default Matrix example in the mapper and you're
  ready to go. 

  This pattern builds on the example "pulse 2D". To best understand this one,
  start there.
*/

/* Each palette is created with http://fastled.io/tools/paletteknife/ and 
http://soliton.vm.bytemark.co.uk/pub/cpt-city/index.html , then normalized 
to 0.0 to 1.0 range. 
*/

var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]
  
arrayMutate(black_Blue_Magenta_White_gp,(v, i ,a) => v / 255);  

var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]
  
arrayMutate(es_landscape_33_gp,(v, i ,a) => v / 255);

var bhw1_05_gp = [
    0,   1,221, 53,
  255,  73,  3,178]

arrayMutate(bhw1_05_gp,(v, i ,a) => v / 255);

var bhw1_04_gp = [
    0, 229,227,  1,
   15, 227,101,  3,
  142,  40,  1, 80,
  198,  17,  1, 79,
  255,   0,  0, 45]

arrayMutate(bhw1_04_gp,(v, i ,a) => v / 255);

var palettes = [black_Blue_Magenta_White_gp, es_landscape_33_gp, bhw1_05_gp, bhw1_04_gp]
var numPalettes = arrayLength(palettes)
export var paletteIndex = 0;
export var timeAccumulator = 0.0;

// primarily useful for testing, go to the next palette in the main array.
export function triggerIncrementPalette(){
  paletteIndex = (paletteIndex + 1) % numPalettes;
}

// arrays to hold rgb interpolation results
var pixel1 = array(3);
var pixel2 = array(3);

// user space version of Pixelblaze's paint function. Stores
// interpolated rgb color in rgbArray
function paint2(v, rgbArray, pal) {
  var k,u,l;
  var rows = pal.length / 4;

  // find the top bounding palette row
  for (i = 0; i < rows;i++) {
    k = pal[i * 4];
    if (k >= v) break;
  }

  // fast path for special cases
  if ((i == 0) || (i >= rows) || (k == v)) {
    i = 4 * min(rows - 1, i);
    rgbArray[0] = pal[i+1];
    rgbArray[1] = pal[i+2];
    rgbArray[2] = pal[i+3];    
  }
  else {
    i = 4 * (i-1);
    l = pal[i]   // lower bound    
    u = pal[i+4]; // upper bound

    pct = 1 -(u - v) / (u-l);
    
    rgbArray[0] = mix(pal[i+1],pal[i+5],pct);
    rgbArray[1] = mix(pal[i+2],pal[i+6],pct);
    rgbArray[2] = mix(pal[i+3],pal[i+7],pct);    
  }
}

// interpolate colors within and between two palettes
function paletteMix(pal1, pal2, colorPct, palettePct) {
  paint2(colorPct,pixel1,pal1);
  paint2(colorPct,pixel2,pal2);  
  
  rgb(mix(pixel1[0],pixel2[0],palettePct),
      mix(pixel1[1],pixel2[1],palettePct),
      mix(pixel1[2],pixel2[2],palettePct)
   )
}


export function beforeRender(delta) {

    // get accumulated run time in seconds, wrapping every hour
 // to keep fixed-point precision in a "good" range.
  timeAccumulator = (timeAccumulator+delta/1000) % 3600;
  
  // switch palettes every five seconds
  if (timeAccumulator > 8) {
    paletteIndex = (paletteIndex + 1) % numPalettes;
    timeAccumulator = 0;
  }

  // tell Pixelblaze which palette to use
  setPalette(palettes[paletteIndex])
  
  tf = 5 // Overall animation duration constant. A smaller duration runs faster.
  
  f  = wave(time(tf * 6.6 / 65.536)) * 5 + 2 // 2 to 7; Frequency (cell density)
  t1 = wave(time(tf * 9.8 / 65.536)) * PI2  // 0 to 2*PI; Oscillates x shift
  t2 = wave(time(tf * 12.5 / 65.536)) * PI2 // 0 to 2*PI; Oscillates y shift
  t3 = wave(time(tf * 9.8 / 65.536)) // Shift h: wavelength of tf * 9.8 s
  t4 = time(tf * 0.66 / 65.536) // Shift v: 0 to 1 every 0.66 sec
  

}

export function render2D(index, x, y) {
  z = (1 + sin(x * f + t1) + cos(y * f + t2)) * .5 

  /*
    As explained in "Matrix 2D Pulse", z is now an egg-carton shaped surface
    in x and y. The number of hills/valles visible (the frequency) is
    proportional to f; f oscillates. The position of the centers in x and y 
    oscillate with t1 and t2. z's value ranges from -0.5 to 1.5.
    
    First, we'll derive the brightness (v) from this field.
    
    t4 is a 0 to 1 sawtooth, so (z + t4) now is between -0.5 and 2.5 wave(z +
    t4) therefore cycles 0 to 1 three times, ever shifting (by t4) with respect
    to the original egg carton.
  */
  v = wave(z + t4)
  
  // Typical concave-upward brightness scaling for perceptual aesthetics.
  // v enters and exits as 0-1. 0 -> 0, 1 -> 1, but 0.5 -> 0.125 
  v = v * v * v
  
  /*
    Triangle will essentially double the frequency; t3 will add an 
    oscillating offset. With h in 0-1.5, hsv() "wraps" h, and since all
    these functions are continuous, it's just spending extra time on the
    hue wheel in the 0-0.5 range. Tweak this until you like how the final 
    colors progress over time, but anything based on z will make colors
    related to the circles seen from above in the egg carton pattern.
  */
  h = triangle(z) / 2 + t3
  
  // original code does HSV. Using this turns off all palettes.
  //hsv(h, 1, v)
  
  /* paint does what we want, but doesn't blend the palettes. 
  With this code, the pattern "works" but doesn't include any blending.
  */
  
  paint(h,v)
  
  /* this is the code that starts the process of doing this with blending palettes
  but does NOT work right now */
  
  // pct = frac(h + index/pixelCount)
  // paletteMix(palettes[0],palettes[1],pct,wave(h));


}

/*
  When there's no map defined, Pixelblaze will call render() instead of 
  render2D() or render3D(), so it's nice to define a graceful degradation for 1D
  strips. For many geometric patterns, you'll want to define a projection down a
  dimension. 
*/
export function render(index) {
  pct = index / pixelCount  // Transform index..pixelCount to 0..1
  // render2D(index, pct, pct)  // Render the diagonal of a matrix
  // render2D(index, pct, 0)    // Render the top row of a matrix
  render2D(index, 3 * pct, 0)   // Render 3 top rows worth to make it denser
}

// You can also project up a dimension. Think of this as mixing in the z value
// to x and y in order to compose a stack of matrices.
export function render3D(index, x, y, z) {
  x1 = (x - cos(z / 4 * PI2)) / 2
  y1 = (y - sin(z / 4 * PI2)) / 2
  render2D(index, x1, y1)
}

So, this gets a little complicated.

The previous code block wasn’t intended as a ready-to-run solution. The interesting bits were a couple of functions – paint2(), which gives you access to the interpolated palette color that paint() would produce (and doesn’t just set an LED to that color) and paletteMix(), which shows how to generate a blended color from two palettes. Just basic building blocks for palette blending.

For a more generic solution, you need to contend with palette selection along with managing hold and transition times. Click on “Gradient Palette Blending Demo” below to see one way of doing this. I’ve uploaded it to the library as well.

The demo shows a super simple animation of the current color set, while cycling through three palettes with user-adjustable hold and transition times.

Blending is done by computing intermediate palettes in beforeRender(), so impact on frame rate is minimal. You can add as many palettes as you have memory for, and your render() function doesn’t have to know anything about this - it should just call paint() as usual.

Gradient Palette Blending Demo
// Fast(er) palette blending demo. Switches between multiple palettes
// at a configurable interval, and blends them while switching with
// configurable transition time.  Also shows how to convert FastLED
// gradient palettes for use with Pixelblaze.
//
// MIT License - Have fun!
//
// 6/03/2023 ZRanger1

// a bunch of fastled gradient palettes
var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]
// normalize palette to 0.0 to 1.0 range  
arrayMutate(black_Blue_Magenta_White_gp,(v, i ,a) => v / 255);  

var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]
// normalize palette to 0.0 to 1.0 range    
arrayMutate(es_landscape_33_gp,(v, i ,a) => v / 255);

var heatmap_gp = [
  0,     0,  0,  0,   
128,   255,  0,  0,   
224,   255,255,  0,   
255,   255,255,255 ];
// normalize palette to 0.0 to 1.0 range   
arrayMutate(heatmap_gp,(v, i ,a) => v / 255);

// list of the palettes we'll be using
var palettes = [black_Blue_Magenta_White_gp,es_landscape_33_gp,heatmap_gp]

// control variables for palette switch timing (these are in seconds)
var PALETTE_HOLD_TIME = 5
var PALETTE_TRANSITION_TIME = 2;

// internal variables used by the palette manager.
// Usually not necessary to change these.
var currentIndex = 0;
var nextIndex = (currentIndex + 1) % palettes.length;

// arrays to hold rgb interpolation results
var pixel1 = array(3);
var pixel2 = array(3);

// array to hold calculated blended palette
var PALETTE_SIZE = 16;
var currentPalette = array(4 * PALETTE_SIZE)

// timing related variables
var inTransition = 0;
var blendValue = 0;
runTime = 0

// Startup initialization for palette manager
setPalette(currentPalette);
buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)  

// user space version of Pixelblaze's paint function. Stores
// interpolated rgb color in rgbArray
function paint2(v, rgbArray, pal) {
  var k,u,l;
  var rows = pal.length / 4;

  // find the top bounding palette row
  for (i = 0; i < rows;i++) {
    k = pal[i * 4];
    if (k >= v) break;
  }

  // fast path for special cases
  if ((i == 0) || (i >= rows) || (k == v)) {
    i = 4 * min(rows - 1, i);
    rgbArray[0] = pal[i+1];
    rgbArray[1] = pal[i+2];
    rgbArray[2] = pal[i+3];    
  }
  else {
    i = 4 * (i-1);
    l = pal[i]   // lower bound    
    u = pal[i+4]; // upper bound

    pct = 1 -(u - v) / (u-l);
    
    rgbArray[0] = mix(pal[i+1],pal[i+5],pct);
    rgbArray[1] = mix(pal[i+2],pal[i+6],pct);
    rgbArray[2] = mix(pal[i+3],pal[i+7],pct);    
  }
}

// utility function:
// interpolate colors within and between two palettes
// and set the LEDs directly with the result.  To be
// used in render() functions
function paletteMix(pal1, pal2, colorPct,palettePct) {
  paint2(colorPct,pixel1,pal1);
  paint2(colorPct,pixel2,pal2);  
  
  rgb(mix(pixel1[0],pixel2[0],palettePct),
      mix(pixel1[1],pixel2[1],palettePct),
      mix(pixel1[2],pixel2[2],palettePct)
   )
}

// construct a new palette in the currentPalette array by blending 
// between pal1 and pal2 in proportion specified by blend
function buildBlendedPalette(pal1, pal2, blend) {
  var entry = 0;
  
  for (var i = 0; i < PALETTE_SIZE;i++) {
    var v = i / PALETTE_SIZE;
    
    paint2(v,pixel1,pal1);
    paint2(v,pixel2,pal2);  
    
    // build new palette at currrent blend level
    currentPalette[entry++] = v;
    currentPalette[entry++] = mix(pixel1[0],pixel2[0],blend)
    currentPalette[entry++] = mix(pixel1[1],pixel2[1],blend)
    currentPalette[entry++] = mix(pixel1[2],pixel2[2],blend)    
  }
}
  
export function beforeRender(delta) {
  runTime = (runTime + delta / 1000) % 3600;

  // Palette Manager - handle palette switching and blending with a 
  // tiny state machine  
  if (inTransition) {
    if (runTime >= PALETTE_TRANSITION_TIME) {
      // at the end of a palette transition, switch to the 
      // next set of palettes and reset everything for the
      // normal hold period.
      runTime = 0;
      inTransition = 0
      blendValue = 0
      currentIndex = (currentIndex + 1) % palettes.length
      nextIndex = (nextIndex + 1) % palettes.length   

    }
    else {
      // evaluate blend level during transition
      blendValue = runTime / PALETTE_TRANSITION_TIME
    }
    
    // blended palette is only recalculated during transition times. The rest of 
    // the time, we run with the current palette at full speed.
    buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)          
  }
  else if (runTime >= PALETTE_HOLD_TIME) {
    // when hold period ends, switch to palette transition
    runTime = 0
    inTransition = 1
  }
  
  // beforeRender() code specific to your pattern can go below this line

}

// Add your pattern render() code here -- just use paint to get color
// from the current blended palette.
export function render(index) {
  pct = frac(wave(time(0.1))+ index/pixelCount)
  paint(pct);
}
1 Like

Holy cow, I know around here @wizard is the official Wizard, but this is incredible, @zranger1 !

It works BEAUTIFULLY.

Since you’re putting it in the pattern library, I’d suggest two clarification edits for future users – for people who don’t know how FastLED gradient palettes and cpt-city works, I’d include:

/* Each palette is created with http://fastled.io/tools/paletteknife/ and 
http://soliton.vm.bytemark.co.uk/pub/cpt-city/index.html , then normalized 
to 0.0 to 1.0 range. 
*/

In order to give people a reference to the tools they need to make their own, and I found this helper button extremely helpful in testing:

// primarily useful for testing, go to the next palette in the main array. Skips the blend step. 
export function triggerIncrementPalette(){
  currentIndex = (currentIndex + 1) % palettes.length;
}

When paired with adding an “export” for currentIndex, so it’s possible to figure out what palettes are in use.

If anyone wants to go full script-kiddy and use the mashup of Honeycomb 2D and this new Gradient Palette Blending, have at it, the pattern is below, and includes seven different palettes I personally like / find ascetically pleasing when used in this way, since figuring out what palettes look good instead of garish can be tricky. This is a potential time-saving step for other people doing this; I’ve also commented them to be clear what they look like.

/*
  Honeycomb 2D - with dynamic palettes
  This code takes the Honeycomb 2d code and makes it rotate between different 
  palettes. It is based on the original Honeycomb 2D code, merged together and tweaked 
  by ZacharyRD with ZRanger1's Gradient Palette Blending Demo code, both 
  available in the PixelBlaze Pattern Library. 

  This pattern is meant to be displayed on an LED matrix or other 2D surface
  defined in the Mapper tab, but also has 1D and 3D renderers defined.
  
  Output demo: https://youtu.be/u9z8_XGe684
  
  The mapper allows us to share patterns that work in 2D or 3D space without the
  pattern code being dependent on how the LEDs were wired or placed in space.
  That means these three installations could all render the same pattern after
  defining their specific LED placements in the mapper:
    
    1. A 8x8 matrix in a perfect grid, wired the common zigzag way
    2. Individual pixels on a strand mounted in a triangle hexagon grid
    3. Equal length strips wired as vertical columns on separate channels
         of the output expander board
  
  To get started quickly with matrices, there's an inexpensive 8x8 on the 
  Pixelblaze store. Load the default Matrix example in the mapper and you're
  ready to go. 

  This pattern builds on the example "pulse 2D". To best understand this one,
  start there.
*/

/* Each palette is created with http://fastled.io/tools/paletteknife/ and 
http://soliton.vm.bytemark.co.uk/pub/cpt-city/index.html , then normalized 
to 0.0 to 1.0 range. 
*/

//http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html
//black-blue-purple-pink-white
var black_Blue_Magenta_White_gp = [
    0,   0,  0,  0,
   42,   0,  0, 45,
   84,   0,  0,255,
  127,  42,  0,255,
  170, 255,  0,255,
  212, 255, 55,255,
  255, 255,255,255]
  
arrayMutate(black_Blue_Magenta_White_gp,(v, i ,a) => v / 255);  

//http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_33.png.index.html
//brown-yellow-forest-green
var es_landscape_33_gp = [
    0,   1,  5,  0,
   19,  32, 23,  1,
   38, 161, 55,  1,
   63, 229,144,  1,
   66,  39,142, 74,
  255,   1,  4,  1]
  
arrayMutate(es_landscape_33_gp,(v, i ,a) => v / 255);

//http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_05.png.index.html
//teal to purple
var bhw1_05_gp = [
    0,   1,221, 53,
  255,  73,  3,178]

arrayMutate(bhw1_05_gp,(v, i ,a) => v / 255);

//http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html
//yellow-orange-purple-navy
var bhw1_04_gp = [
    0, 229,227,  1,
   15, 227,101,  3,
  142,  40,  1, 80,
  198,  17,  1, 79,
  255,   0,  0, 45]

arrayMutate(bhw1_04_gp,(v, i ,a) => v / 255);

//http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html
//red-orange-pink-purple-blue
var Sunset_Real_gp = [
    0, 120,  0,  0,
   22, 179, 22,  0,
   51, 255,104,  0,
   85, 167, 22, 18,
  135, 100,  0,103,
  198,  16,  0,130,
  255,   0,  0,160]

arrayMutate(Sunset_Real_gp,(v, i ,a) => v / 255);

// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_3.png.index.html
//purple pink red, with more purple than red. 
var Analogous_3_gp = [
    0,  67, 55,255,
   63,  74, 25,255,
  127,  83,  7,255,
  191, 153,  1, 45,
  255, 255,  0,  0]

arrayMutate(Analogous_3_gp,(v, i ,a) => v / 255);

// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html
//blue-purple-red evenly split out. 
var Analogous_1_gp = [
    0,   3,  0,255,
   63,  23,  0,255,
  127,  67,  0,255,
  191, 142,  0, 45,
  255, 255,  0,  0]

arrayMutate(Analogous_1_gp,(v, i ,a) => v / 255);


var palettes = [black_Blue_Magenta_White_gp, es_landscape_33_gp, bhw1_05_gp, bhw1_04_gp, Sunset_Real_gp, Analogous_3_gp, Analogous_1_gp]

// control variables for palette switch timing (these are in seconds)
var PALETTE_HOLD_TIME = 10
var PALETTE_TRANSITION_TIME = 3;

// internal variables used by the palette manager.
// Usually not necessary to change these.
export var currentIndex = 0;
var nextIndex = (currentIndex + 1) % palettes.length;

// primarily useful for testing, go to the next palette in the main array. Skips the blend step. 
export function triggerIncrementPalette(){
  currentIndex = (currentIndex + 1) % palettes.length;
}

// arrays to hold rgb interpolation results
var pixel1 = array(3);
var pixel2 = array(3);

// array to hold calculated blended palette
var PALETTE_SIZE = 16;
var currentPalette = array(4 * PALETTE_SIZE)

// timing related variables
var inTransition = 0;
var blendValue = 0;
runTime = 0

// Startup initialization for palette manager
setPalette(currentPalette);
buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)  

// user space version of Pixelblaze's paint function. Stores
// interpolated rgb color in rgbArray
function paint2(v, rgbArray, pal) {
  var k,u,l;
  var rows = pal.length / 4;

  // find the top bounding palette row
  for (i = 0; i < rows;i++) {
    k = pal[i * 4];
    if (k >= v) break;
  }

  // fast path for special cases
  if ((i == 0) || (i >= rows) || (k == v)) {
    i = 4 * min(rows - 1, i);
    rgbArray[0] = pal[i+1];
    rgbArray[1] = pal[i+2];
    rgbArray[2] = pal[i+3];    
  }
  else {
    i = 4 * (i-1);
    l = pal[i]   // lower bound    
    u = pal[i+4]; // upper bound

    pct = 1 -(u - v) / (u-l);
    
    rgbArray[0] = mix(pal[i+1],pal[i+5],pct);
    rgbArray[1] = mix(pal[i+2],pal[i+6],pct);
    rgbArray[2] = mix(pal[i+3],pal[i+7],pct);    
  }
}

// utility function:
// interpolate colors within and between two palettes
// and set the LEDs directly with the result.  To be
// used in render() functions
function paletteMix(pal1, pal2, colorPct,palettePct) {
  paint2(colorPct,pixel1,pal1);
  paint2(colorPct,pixel2,pal2);  
  
  rgb(mix(pixel1[0],pixel2[0],palettePct),
      mix(pixel1[1],pixel2[1],palettePct),
      mix(pixel1[2],pixel2[2],palettePct)
   )
}

// construct a new palette in the currentPalette array by blending 
// between pal1 and pal2 in proportion specified by blend
function buildBlendedPalette(pal1, pal2, blend) {
  var entry = 0;
  
  for (var i = 0; i < PALETTE_SIZE;i++) {
    var v = i / PALETTE_SIZE;
    
    paint2(v,pixel1,pal1);
    paint2(v,pixel2,pal2);  
    
    // build new palette at currrent blend level
    currentPalette[entry++] = v;
    currentPalette[entry++] = mix(pixel1[0],pixel2[0],blend)
    currentPalette[entry++] = mix(pixel1[1],pixel2[1],blend)
    currentPalette[entry++] = mix(pixel1[2],pixel2[2],blend)    
  }
}

export function beforeRender(delta) {
  runTime = (runTime + delta / 1000) % 3600;

  // Palette Manager - handle palette switching and blending with a 
  // tiny state machine  
  if (inTransition) {
    if (runTime >= PALETTE_TRANSITION_TIME) {
      // at the end of a palette transition, switch to the 
      // next set of palettes and reset everything for the
      // normal hold period.
      runTime = 0;
      inTransition = 0
      blendValue = 0
      currentIndex = (currentIndex + 1) % palettes.length
      nextIndex = (nextIndex + 1) % palettes.length   

    }
    else {
      // evaluate blend level during transition
      blendValue = runTime / PALETTE_TRANSITION_TIME
    }
    
    // blended palette is only recalculated during transition times. The rest of 
    // the time, we run with the current palette at full speed.
    buildBlendedPalette(palettes[currentIndex],palettes[nextIndex],blendValue)          
  }
  else if (runTime >= PALETTE_HOLD_TIME) {
    // when hold period ends, switch to palette transition
    runTime = 0
    inTransition = 1
  }
  
  tf = 5 // Overall animation duration constant. A smaller duration runs faster.
  
  f  = wave(time(tf * 6.6 / 65.536)) * 5 + 2 // 2 to 7; Frequency (cell density)
  t1 = wave(time(tf * 9.8 / 65.536)) * PI2  // 0 to 2*PI; Oscillates x shift
  t2 = wave(time(tf * 12.5 / 65.536)) * PI2 // 0 to 2*PI; Oscillates y shift
  t3 = wave(time(tf * 9.8 / 65.536)) // Shift h: wavelength of tf * 9.8 s
  t4 = time(tf * 0.66 / 65.536) // Shift v: 0 to 1 every 0.66 sec
  

}

export function render2D(index, x, y) {
  z = (1 + sin(x * f + t1) + cos(y * f + t2)) * .5 

  /*
    As explained in "Matrix 2D Pulse", z is now an egg-carton shaped surface
    in x and y. The number of hills/valles visible (the frequency) is
    proportional to f; f oscillates. The position of the centers in x and y 
    oscillate with t1 and t2. z's value ranges from -0.5 to 1.5.
    
    First, we'll derive the brightness (v) from this field.
    
    t4 is a 0 to 1 sawtooth, so (z + t4) now is between -0.5 and 2.5 wave(z +
    t4) therefore cycles 0 to 1 three times, ever shifting (by t4) with respect
    to the original egg carton.
  */
  v = wave(z + t4)
  
  // Typical concave-upward brightness scaling for perceptual aesthetics.
  // v enters and exits as 0-1. 0 -> 0, 1 -> 1, but 0.5 -> 0.125 
  v = v * v * v
  
  /*
    Triangle will essentially double the frequency; t3 will add an 
    oscillating offset. With h in 0-1.5, hsv() "wraps" h, and since all
    these functions are continuous, it's just spending extra time on the
    hue wheel in the 0-0.5 range. Tweak this until you like how the final 
    colors progress over time, but anything based on z will make colors
    related to the circles seen from above in the egg carton pattern.
  */
  h = triangle(z) / 2 + t3
  
  // original code does HSV. Using this instead of paint turns off all palettes.
  //hsv(h, 1, v)
  
  paint(h,v)

}

/*
  When there's no map defined, Pixelblaze will call render() instead of 
  render2D() or render3D(), so it's nice to define a graceful degradation for 1D
  strips. For many geometric patterns, you'll want to define a projection down a
  dimension. 
*/
export function render(index) {
  pct = index / pixelCount  // Transform index..pixelCount to 0..1
  // render2D(index, pct, pct)  // Render the diagonal of a matrix
  // render2D(index, pct, 0)    // Render the top row of a matrix
  render2D(index, 3 * pct, 0)   // Render 3 top rows worth to make it denser
}

// You can also project up a dimension. Think of this as mixing in the z value
// to x and y in order to compose a stack of matrices.
export function render3D(index, x, y, z) {
  x1 = (x - cos(z / 4 * PI2)) / 2
  y1 = (y - sin(z / 4 * PI2)) / 2
  render2D(index, x1, y1)
}

Ok - the super simple version is in the Electromage library, and a revised and slightly enhanced version is now available from my github patterns repo.

3 Likes

I finished this project and wrote it up as a beginner tutorial here: Beginners Guide to Making a LED Festival Coat

5 Likes