Code for Task #5: Rainbow Codenections!

Post your answers to Task #5 here, so we avoid spoilers for others.

To celebrate the coming matrix tasks, here’s a rainbow pattern that uses many sliders and works in 2D as well as on a strip. It’s based on code that @scruffynerf and I put together for this thread:

Without further ado, the code:

// Rainbow and radial rainbow for 1D and 2D displays
export var speed = 0.25
export var direction = 1
export var scale = 2.9

// UI sliders

// higher is faster
export function sliderSpeed(v) {
  speed = 1-v;  
}

// left = inward, right = outward
export function sliderDirection(v) {
  direction = (v < 0.5) ? 1 : -1;  
}

export function sliderBandwidth(v) {
  scale = sqrt(0.5) * 5 * (1-v);
}

// pythagorean distance from center of display for 2D.  Pixelblaze
// provides normalized x,y coords, so center is always going
// to be (0.5,0.5) regardless of real world display dimensions
function getRadius(x, y) {
  x -= 0.5; y -= 0.5;
  return sqrt(x*x + y*y);
}

// generate a timer - a sawtooth wave that we can
// use to animate color -- the direction flag makes
// it positive or negative, depending on the UI
// slider setting
export function beforeRender(delta) {
  t1 = direction * time(0.08 * speed);
}

// use wave starting from center and our timer to color every pixel
export function render(index) {
  hsv(t1+(wave(0.5*index/pixelCount)*scale), 1, 1);
}

// use radius and our timer to color every pixel
export function render2D(index, x, y) {
  hsv(t1+(getRadius(x, y)*scale), 1, 1);
}
2 Likes

Here’s what I know:

  • I ported HSLuv to PB to chase the perfect rainbow
  • This was ridiculous scope creep, it took forever
  • I learned stuff
  • If you like unicorns and pastels, you’ll luv HSLuv
  • It requires PB v3 and I appreciate the new array functions

Here’s what I don’t know:

:thinking:

Click to show the the code 😬
/*
  !! This pattern requires Pixelblaze v3
  
  This is a port of HSLuv to Pixelblaze's 16.16 fixed point math.
  
  HSLuv is a perceptally equidistant colorspace with fewer tradeoffs than CIELUV
  
  https://www.hsluv.org/
  
  If you've ever been in search of a more-perfect rainbow than hsv(h, 1, 1)
  generates, this is for you.
  
  For some background, see 
  https://forum.electromage.com/t/color-palette-support/192/6?u=jeff
  
  The math is very intense. A simple rainbow generator goes from 430 
  to 15 FPS using this! Therefore, you may wish to precompute and cache
  a lookup table of some sort. More reasonable approaches WRT performance
  are shown in the thread above and the Perceptual Hue pattern.
    
  HSLuv is released under an MIT licesne, as is this.
  
  Jeff Vyduna
*/


var speed = .3, saturation = 1, luminosity = .5

export function sliderSpeed(v) { speed = .0001 + v }
export function sliderSaturation(v) { saturation = v }
export function sliderLuminosity(v) { luminosity = v }

var hsluv = array(3)
var rgbResult = array(3)

var reds = array(pixelCount)
var greens = array(pixelCount)
var blues = array(pixelCount)

var elapsed = 0 // ms
export function beforeRender(delta) {
  elapsed += delta
  // I can't get precompute() to work outside of beforeRender, like on 
  // slider change (execution steps exhausted)
  if (elapsed > 100) {
    precompute()
    elapsed -= 100
  }
  t1 = time(.02 / speed)
}

export function render(index) {
  // Live-computation version
  // h = (t1 + index/pixelCount) % 1
  // hsluv[0] = h * 360
  // rgbResult = hsluvToRgb(hsluv)
  // rgb(rgbResult[0], rgbResult[1], rgbResult[2])
  
  animIndex = (index + t1 * pixelCount) % pixelCount 
  rgb(reds[animIndex], greens[animIndex], blues[animIndex])
}


// Begin port of https://github.com/hsluv/hsluv/blob/master/haxe/src/hsluv/Hsluv.hx

// Constants

var m = array(3); m.mutate(() => array(3))
m[0][0] = 3.240969941904521
m[0][1] = -1.537383177570093
m[0][2] = -.498610760293
m[1][0] = -.96924363628087
m[1][1] = 1.87596750150772
m[1][2] = .041555057407175
m[2][0] = .055630079696993
m[2][1] = -.20397695888897
m[2][2] = 1.056971514242878

var minv = array(3); minv.mutate(() => array(3))
minv[0][0] = .41239079926595
minv[0][1] = .35758433938387
minv[0][2] = .18048078840183
minv[1][0] = .21263900587151
minv[1][1] = .71516867876775
minv[1][2] = .072192315360733
minv[2][0] = .019330818715591
minv[2][1] = .11919477979462
minv[2][2] = .95053215224966

var refY = 1,
    refU = .19783000664283,
    refV = .46831999493879,
    kappa = 903.2962962,
    epsilon =.0088564516
    // hexChars = "0123456789abcdef"  // When PB supports chars, we can add HTML hex codes
  
/**
  For a given lightness, return a list of 6 lines in slope-intercept
  form that represent the bounds in CIELUV, stepping over which will
  push a value out of the RGB gamut
  
  float64 converted to 16.16 by dividing all constants by 10000
*/
var boundResult = array(6)
boundResult.mutate(() => array(2))
function getBounds(L) {
  var sub1 = pow((L + 16) / 116, 3)
  var sub2 = sub1 > epsilon ? sub1 : L / kappa
  
  for(var c = 0; c < 3; c++) {
    var m1 = m[c][0], m2 = m[c][1], m3 = m[c][2]
    for(var t = 0; t < 2; t++){
      var top1 = (28.4517 * m1 - 9.4839 * m3) * sub2
      var top2 = (83.8422 * m3 + 76.9860 * m2 + 73.1718 * m1) * L * sub2 - 76.9860 * t * L
      var bottom = (63.2260 * m3 - 12.6452 * m2) * sub2 + 12.6452 * t

      boundResult[c * 2 + t][0] = top1 / bottom // slope
      boundResult[c * 2 + t][1] = top2 / bottom // intercept
    }
  }
  return boundResult
}

/**
  For given lightness, returns the maximum chroma. Keeping the chroma value
  below this number will ensure that for any hue, the color is within the RGB
  gamut.
*/
function maxSafeChromaForL(L) {
  var bounds = getBounds(L)
  var minResult = 32767 // Math.POSITIVE_INFINITY
  
  for(var bound = 0; bound < bounds.length; bound++) { 
    var slope = bounds[bound][0]
    var intercept = bounds[bound][1]
    var length = abs(slope) / sqrt(intercept * intercept + 1)
    minResult = min(minResult, length)
  }
  return minResult
}

function maxChromaForLH(L, H) {
  var hrad = H / 360 * PI2
  var bounds = getBounds(L)
  var minResult = 32767 // Math.POSITIVE_INFINITY
  
  for(var bound = 0; bound < bounds.length; bound++) { 
    var slope = bounds[bound][0]
    var intercept = bounds[bound][1]
    var length = intercept / (sin(hrad) - slope * cos(hrad))
    if (length >= 0) minResult = min(minResult, length)
  }
  return minResult
}

function dotProduct(a, b) {
  var sum = 0
  for(var i = 0; i < a.length; i++) sum += a[i] * b[i]
  return sum
}

// Used for rgb conversions
function fromLinear(c) {
  return c <= .0031308 ? 12.92 * c : 1.055 * pow(c, .4166666666666667) - .055
}

function toLinear(c){
  return c > 0.04045 ? pow((c + .055) / 1.055, 2.4) : c / 12.92
}

/**
* XYZ coordinates are ranging in [0;1] and RGB coordinates in [0;1] range.
* xyz: An array containing the color's X,Y and Z values.
* Returns an array containing the resulting color's red, green and blue.
**/
var rgbResult = array(3)
function xyzToRgb(xyz){
  xyz.mapTo(rgbResult, (v, i, arr) =>
    fromLinear(dotProduct(m[i], arr))
  )
  return rgbResult
}

/**
* RGB coordinates are ranging in [0;1] and XYZ coordinates in [0;1].
* rgb: An array containing the color's R,G,B values.
* Returns an array containing the resulting color's XYZ coordinates.
**/
var xyzResult = array(3)
var rgbl = array(3)
function rgbToXyz(rgb) {
  rgb.mapTo(rgbl, (v) => toLinear(v))
  
  rgbl.mapTo(xyzResult, function(v, i, arr) {
    dotProduct(minv[i], arr)
  })
  
  return xyzResult
}

/**
* XYZ coordinates are ranging in [0;1].
* xyz: An array containing the color's X,Y,Z values.
* Returns an array containing the resulting color's LUV coordinates.
**/
var luvResult = array(3)
function xyzToLuv(xyz){
  var X = xyz[0], Y = xyz[1], Z = xyz[2]
  // Warning: Did not handle div by 0: https://github.com/hsluv/hsluv/blob/master/haxe/src/hsluv/Hsluv.hx#L207
  var divider = X + (15 * Y) + (3 * Z)
  var varU = 4 * X / divider
  var varV = 9 * Y / divider

  if (Y <= epsilon) {
    var L = (Y / refY) * kappa
  } else {
    var L = 116 * pow(Y / refY, 1/3) - 16
  }
  
  if (L == 0) {
    luvResult[0] = luvResult[1] = luvResult[2] = 0
    return luvResult
  }
  
  luvResult[0] = L
  luvResult[1] = 13 * L * (varU - refU)
  luvResult[2] = 13 * L * (varV - refV)

  return luvResult
}

/**
* XYZ coordinates are ranging in [0;1].
* luv: An array containing the color's L,U,V values.
* Returns an array containing the resulting color's XYZ coordinates.
**/
var xyzResult = array(3)
function luvToXyz(luv){
  var L = luv[0], U = luv[1], V = luv[2]

  if (L == 0) {
    xyzResult[0] = xyzResult[1] = xyzResult[2] = 0
    return xyzResult
  }

  var varU = U / (13 * L) + refU
  var varV = V / (13 * L) + refV

  if (L <= 8) {
    var Y = refY * L / kappa
  } else {
    var Y = refY * pow((L + 16) / 116, 3)
  }

  var X = xyzResult[0] = - (9 * Y * varU) / ((varU - 4) * varV - varU * varV)
  xyzResult[1] = Y
  xyzResult[2] = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV)

  return xyzResult
}

/**
* luv: An array containing the color's L,U,V values.
* returns an array containing the resulting color's LCH coordinates.
**/
var lchResult = array(3)
function luvToLch(luv) {
  var U = luv[1], V = luv[2]
  var H, C = hypot(U, V)
  
  // Greys: disambiguate hue
  if (C < 0.0001) {
    H = 0
  } else {
    var hrad = atan2(V, U)
    H = hrad * 180 / PI
    if (H < 0) H += 360
  }
  
  lchResult[0] = luv[0]
  lchResult[1] = C
  lchResult[2] = H

  return lchResult
}

/**
* lch: An array containing the color's L,C,H values.
* Returns an array containing the resulting color's LUV coordinates.
**/
var luvResult = array(3)
function lchToLuv(lch){
  var L = lch[0], C = lch[1], H = lch[2]
  var hrad = H / 360 * PI2
  luvResult[0] = L
  luvResult[1] = cos(hrad) * C
  luvResult[2] = sin(hrad) * C
  return luvResult
}

/**
* HSLuv values are ranging in [0;360], [0;100] and [0;100].
* hsluv: An array containing the color's H,S,L values in HSLuv color space.
* Returns an array containing the resulting color's LCH coordinates.
**/
var lchResult = array(3)
function hsluvToLch(hsluv){
  var H = hsluv[0], S = hsluv[1], L = hsluv[2]
  lchResult[1] = 0
  lchResult[2] = H
  
  // White and black: disambiguate chroma
  if (L > 99.9999) { lchResult[0] = 100; return lchResult }
  if (L < 0.001) { lchResult[0] = 0; return lchResult }
  lchResult[0] = L
  lchResult[1] = maxChromaForLH(L, H) / 100 * S
  return lchResult
}

/**
* HSLuv values are ranging in [0;360], [0;100] and [0;100].
* lch: An array containing the color's LCH values.
* Returns an array containing the resulting color's HSL coordinates in HSLuv color space.
**/
var hsluvResult = array(3)
function lchToHsluv(lch) {
  var L = lch[0], C = lch[1], H = lch[2]
  
  hsluvResult[0] = H
  hsluvResult[1] = 0
  if(L > 99.9999) { hsluvResult[2] = 100; return hsluvResult }
  if(L < 0.0001) { hsluvResult[2] = 0; return hsluvResult }
  
  hsluvResult[1] = C / maxChromaForLH(L, H) * 100
  hsluvResult[2] = L
  return hsluvResult
}

/**
* HSLuv values are in [0;360], [0;100] and [0;100].
* hpluv: An array containing the color's H,S,L values in HPLuv (pastel variant) color space.
* Returns an array containing the resulting color's LCH coordinates.
**/
var lchResult = array(3)
function hpluvToLch(hpluv) {
  var H = hpluv[0], S = hpluv[1], L = hpluv[2]
  lchResult[1] = 0
  lchResult[2] = H
  
  if (L > 99.99999) { lchResult[0] = 100; return lchResult }
  if (L < .0001) { lchResult[0] = 0; return lchResult }
  
  lchResult[0] = L
  lchResult[1] = maxSafeChromaForL(L) / 100 * S
  return lchResult
}


/**
* HSLuv values are ranging in [0;360], [0;100] and [0;100].
* lch: An array containing the color's LCH values.
* Returns an array containing the resulting color's HSL coordinates in HPLuv (pastel variant) color space.
**/
var hpluvResult = array(3)
function lchToHpluv(lch) {
  var L = lch[0], C = lch[1], H = lch[2]
  hpluvResult[0] = H
  hpluvResult[1] = 0
  hpluvResult[2] = L
  
  if (L > 99.9999) { hpluvResult[2] = 100; return hpluvResult }
  if (L < 0.0001) { hpluvResult[2] = 0; return hpluvResult }
  
  hpluvResult[1] = C / maxSafeChromaForL(L) * 100
  return hpluvResult
}

// function rgbToHex(a){ // Cannot implement on Pixelblaze yet }
// function hexToRgb(a){ // Cannot implement on Pixelblaze yet }

/**
* RGB values are ranging in [0;1].
* lch: An array containing the color's LCH values.
* Returns an array containing the resulting color's RGB coordinates.
**/
function lchToRgb(lch){
  return xyzToRgb(luvToXyz(lchToLuv(lch)))
}

/**
* RGB values are ranging in [0;1].
* rgb: An array containing the color's RGB values.
* Returns an array containing the resulting color's LCH coordinates.
**/
function rgbToLch(rgb) {
  return luvToLch(xyzToLuv(rgbToXyz(rgb)))
}

// RGB <--> HPLuv
 
/**
* HSLuv values are ranging in [0;360], [0;100] and [0;100] and RGB in [0;1].
* hsluv: An array containing the color's HSL values in HSLuv color space.
* Returns an array containing the resulting color's RGB coordinates.
**/
function hsluvToRgb(hsluv){ return lchToRgb(hsluvToLch(hsluv)) }

function rgbToHsluv(rgb){ return lchToHsluv(rgbToLch(rgb)) }
function hpluvToRgb(hpluv){ return lchToRgb(hpluvToLch(hpluv)) }
function rgbToHpluv(rgb){ return lchToHpluv(rgbToLch(rgb)) }


// End Port of HSLuv



// Precache expensice hsluv computations

function precompute() {
  hsluv[1] = 100 * saturation
  hsluv[2] = 100 * luminosity
  for (i = 0; i < pixelCount; i++) {
    hsluv[0] = i / pixelCount * 360
    rgbResult = hsluvToRgb(hsluv)
    reds[i] = rgbResult[0]
    greens[i] = rgbResult[1]
    blues[i] = rgbResult[2]
  }
}
precompute()

2 Likes