Pattern request. Two moving colors

Hi
I’m not very code savvy and love the PB ease of use for just that. However, I would like a pattern like the ones already available, e.g Color bands, Rainbow fonts or Rainbow melt - but with two colors only. In my case blue and yellow. I dont mind the colors being cross faded through black or white, but the general idea is to have a slow moving/blending pattern based on two colors. If anyone could help me in the right direction I’d appreciate it. Thanks.

Hi Mårten!

Any pattern that outputs a rainbow is probably using hue values across the entire spectrum (hue from 0 to 1). We want some code that can transform that to just yellow (hues centered around 0.166) and blue (0.666).

Let’s first write some code that transforms all hues to either yellow or blue.

// We should manually "wrap" any input hues outside 0..1 first
h = h % 1 + (h < 0)
// If the existing hue was below half, output yellow (0.166)
// If it was over half, output blue
h = h > 0.5 ? 0.166 : 0.666

That transforms this:


into this:

Copy-paste that code right above a pattern’s call to hsv(). For many patterns that’s all you need. For example, it looks decent in many of the default patterns, like “blink fade”.

There are a few ways to get fancier. Maybe you want some hue variation:

Input hue Output hue
0 yellow - something
0.25 yellow
0.499 yellow + something
0.5 blue - something
0.75 blue
0.999 blue + something

Or maybe you want to prevent sudden jumps in transitions between yellow and blue for patterns like “fast pulse”. I have some ideas about these - I’ll work on them later this evening.

It’s a lot of code, but for full, specific control, check out the pattern called “Utility: Palettes”. It’s a lot more code, but it lets you remap a 0-1 hue into any combination of colors and pick how it fades (through white, black, or intermediate hues).

1 Like

Like Jeff, my first approach was to split the hue rainbow in two then make the lower half yellow and the upper half blue. This worked for Color Bands:

export function beforeRender(delta) {
  t1 = time(.5)*2
  t2 = time(.25)
  t3 = time(.15)
}

export function render(index) {
  // "pixelCount / 4" creates 4 bands
  h = index / (pixelCount / 4)
  if ((h % 1) < 0.5) {
    // color 1, hue of 0.167 = yellow
    h = 0.167
  }
  else {
    // color 2, hue of 0.667 = blue
    h = 0.667
  }
  s = wave(-index/3 + t2)
  s = 1-s*s*s*s
  v = wave(index/2 + t3) * wave(index/5 - t3) + wave(index/7 + t3)
  v = v*v*v*v
  hsv(h, s, v)
}

But my simple code for splitting the hue rainbow didn’t work for other patterns. Usually because the hue value went above 1.0. Which I could have fixed in code but then I saw Jeff’s post about the Utility: Palettes pattern. Wow, this palette idea is very useful.

I cloned the Rainbow Melt pattern, added some palette functions and now have a 2 color melt, see below. I set up a simple yellow and blue palette (0.167, 500.667) which when used by the posterize function converts values from 0.0 to 0.499 to yellow (.167 hue) and values from 0.5 to 1.0 to blue (.667 hue). You can use this same technique with any pattern.

And as Jeff said, you can go farther. Use interpolation instead of posterize to make smooth transitions between colors. Add black entries in the palette to create gaps between colors. So many possibilities. See Utility: Palettes for more details and examples.

// Define the output pallete.
pal = array(2)
pal[0] = 0.167; pal[1] = 500.667;
// There's no array.length, so thesize of the palette must be defined.
var paletteSize = 2

hl = pixelCount/2
export function beforeRender(delta) {
  t1 = time(.1)
  t2 = time(0.13)
}

export function render(index) {
  c1 = 1-abs(index - hl)/hl
  c2 = wave(c1)
  c3 = wave(c2 + t1)
  v = wave(c3 + t1)
  v = v*v
  hsv(posterize(c1 + t2, pal), 1, v)
}


// Posterize - lookup discreet outputs for continuous 0...1 inputs
function posterize(h, palette) { return remap(h, palette, 0) }

/* Given x in 0...1 (typically a hue but can be used with saturation or value)
   and a remapping array (usually a color palette) of size paletteSize
   remap x through the palette to other 0...1 y values. 
   mode sets the interpolation style.
   mode = 0: Posterize by looking up a single value for each input region.
   mode = 1: Continuously (linearly) interpolate using the shortest path (-0.5..0.5)
     around a hue color wheel to produce a gradient between hues.
   mode = 2: Interpolate between values without wrapping 1 -> 0. Useful for s or v.
*/
function remap(x, palette, mode) {
  x = (x + 255) % 1 // Wrap hue inputs like hsv() does
  w1 = palette[0]
  firstPosition = floor(palette[0])
  firstY = palette[0] % 1
  lastPosition = floor(palette[paletteSize - 1])
  lastY = palette[paletteSize - 1] % 1
  inputPosition = x * 999
  
  // inputPosition is less than the first 0-999 position or greater than the final one
  if (inputPosition < firstPosition || inputPosition > lastPosition) {
    if (mode == 0) { return lastY }
    sectionPositionDistance = 999 - (lastPosition - firstPosition)
    dY = yDistance(lastY, firstY, mode)
    sectionPct = (((1000 + inputPosition - lastPosition) %  1000) / sectionPositionDistance) % 1
    return (lastY + sectionPct * dY) % 1
  }
  
  // inputPosition is within the positions specified in the palette
  for (i = 0;  i < paletteSize - 1; i++) {
    thisPosition = floor(palette[i])
    nextPosition = floor(palette[i+1])
    if (thisPosition <= inputPosition && inputPosition <= nextPosition ) {
      thisY = palette[i] % 1
      if (mode == 0) { return thisY }
      sectionPositionDistance = nextPosition - thisPosition
      dY = yDistance(thisY, palette[i+1] % 1, mode)
      sectionPct = (inputPosition - thisPosition)/sectionPositionDistance
      return (thisY + sectionPct * dY) % 1
    }
  }
}

/* 
  mode = 0: no-op. Unexpected. Could return 0.
  mode = 1:
     Takes two hues, 0...1, and calculates the shortest signed distance 
     from h1 to h2, which will in -0.5..0.5.
     If h2 > h1 by <0.5, or h1 > h2 by >0.5, the shortest route around 
     the wheel is the positive direction.
  mode = 2:
     Signed distance without wrapping from 1 to 0. Useful for s & v maps
*/
function yDistance(y1, y2, mode) {  
  distance = y2 - y1
  if (mode == 2 || abs(distance) < 0.5) return distance
  if (y2 > y1) return distance - 1
  return distance + 1
}
1 Like

@KanyonKris I’m so glad you were able to grok the Palette code! Thanks for showing another solution.

One thing I readily admit about the palette code is that it can slow things down a lot. I wanted to follow up on what I mentioned earlier about a faster, specific math approach to mapping a 0…1 hue into a yellowish range and a bluish range.

Input hue Output hue
0 yellow - something
0.25 yellow
0.499 yellow + something
0.5 blue - something
0.75 blue
0.999 blue + something

Centered around yellow and blue

Here's some math I worked out (really just a y = mx + b kind of thing) to map to ranges centered on opposite colors.

  // Manually "wrap" any input hues outside 0..1.
  h = h % 1 + (h < 0)

  // Map a 0..1 hue into 2 ranges, color1 ± width, and it's compliment
  color1 = 0.166 // Yellow (compliment is blue)
  width = 0.06
  h = color1 + width * (4 * (h % 0.5) - 1) + (h >= 0.5) / 2
  
  hsv(h, 1, v)

How to also fade-through-black

  // Manually "wrap" any input hues outside 0..1.
  h = h % 1 + (h < 0)
  
  // Fade through black for 2 output hues.
  // Multiply this by existing v values.
  fadeThroughBlack = triangle(2 * h)
  
  // Map a 0..1 hue into 2 ranges, color1 ± width, and it's compliment
  color1 = 0.166 // Yellow (compliment is blue)
  width = 0.06
  h = color1 + width * (4 * (h % 0.5) - 1) + (h >= 0.5) / 2
  
  hsv(h, 1, v * fadeThroughBlack)

Or fade through white

  // Manually "wrap" any input hues outside 0..1.
  h = h % 1 + (h < 0)
  
  // Fade through white for 2 output hues.
  // Multiply this by existing v values.
  fadeThroughWhite = triangle(2 * h)
  // Personal preference: less white
  fadeThroughWhite = sqrt(sqrt(fadeThroughWhite))
  
  // Map a 0..1 hue into 2 ranges, color1 ± width, and it's compliment
  color1 = 0.166 // Yellow (compliment is blue)
  width = 0.06
  h = color1 + width * (4 * (h % 0.5) - 1) + (h >= 0.5) / 2
  
  hsv(h, fadeThroughWhite, v)

Since KanyonKris mentioned the “rainbow melt” pattern, I tried it on that one.

Hopefully by choosing one of these approaches, you’ll be able to turn most example patterns into a 2-color one!

3 Likes

@jeff I’m digging your simpler, more elegant math approach.

Some performance numbers using Rainbow Melt on 60 APA102 LEDs:
320 fps - Original Rainbow Melt
280 fps - If method (like I used for Color Band, see code below)
240 fps - Math method (from Jeff’s post above)
180 fps - Palette method

If method:

  // Manually "wrap" any input hues outside 0..1.
  h = h % 1 + (h < 0)
  if (h < 0.5) {
    // color 1, hue of 0.167 = yellow
    h = 0.167
  }
  else {
    // color 2, hue of 0.667 = blue
    h = 0.667
  }

The If method can use any 2 colors, the math method must use 2 complimentary colors.

The Math method is easier to tweak and has the nice width feature which often looks nicer.

Many good choices.

3 Likes

@jeff I still have a lot to learn about coding. Please tell me if I interpret this line of your code correctly:

h = h % 1 + (h < 0)

I understand modulo division (returns the remainder), but I haven’t seen (h < 0) before. What I think it does is: if h is less than zero, h is “returned” out of the parenthesis, otherwise nothing is “returned”?

1 Like

Thanks for the benchmarking! Super helpful.

The output of (1 > 0) is 1 in Pixelblaze, or 0 for a comparison that is false. The value would be true in languages with a boolean type, and then true would be “cast” into a numeric data type so it can be used with the addition operator. So something similar is happening in Pixelblaze.

But why add 1 if the input hue is negative?

I only learned this as a result of making stuff in Pixelblaze, but here’s three interesting things:

  1. Modulo and remainder are different operations in programming - in PB, h % 1 is actually the remainder operation. It means that when the input hue is negative, the result will be negative. The goal here is to “wrap” the hue correctly into something that would look like a consistent sawtooth between 0 and 1 for any input.
  2. You might think, “OK, so how about I just apply an abs() or something?” and that would… Tada… turn it into the actual mathematical definition of modulo (as long as the divisor is positive). Problem is, that becomes a sawtooth mirrored about the point where h=0.
  3. I found the easiest way to turn it into a consistent sawtooth was to just add 1 if the input is negative.

It get’s pretty deep and is inconsistent across languages. [1] [2]

3 Likes

Wow! Thanks for the extensive reply - and I really appreciate you trying hard to explain it as well. You don’t see that in many forums. I also appreciate the “level up” approach so I can play with it a bit on my own!

This was really helpful. Thank you!

2 Likes