Color blending code

This just came up sorta in another thread and I’ll come back to it but I wanted to save this tidbit:

https://www.easyrgb.com/en/math.php

Conversion from many (including HSV, RGB, hsl, cmyk) to many others

Added: Another good resource

So color blending…

The “right” way to do color blending in LEDs is via RGB:

R+G is Yellow, etc…

See Physics Tutorial: Color Addition

But… that’s pretty counter to what people ‘think’ color mixing is like…

also
Computer RGB values are derived from the square root of photon flux. So as a general function, your math should take that into account. The general function for this for a given channel is:

blendColorValue(a, b, t)
    return sqrt((1 - t) * a^2 + t * b^2)

Where a and b are the color values to blend, and t is a number from 0-1 representing the point in the blend you want between a and b.

If you want to do it cheaper, the average of the two values is usually fine…
aka (a+b)/2

doesn’t factor in Alpha (mixing vs blending depends on Alpha), or gamma correction…

http://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/

then we have Wikipedia:

Which is more than we need to deal with, but ‘Screen’ is basically a blend where it’s more like turning on both lights, rather than changing one light.

So if I mixed a dark red and a dark green, if it’s a “mix” blend, it would be a yellow at the same brightness… if it’s a ‘screen’ blend, it would be brighter yellow, which actually makes sense in some LED contexts… if I had spinning red ring and a spinning green ring, both at (for example) .5 brightness, when they overlap, do I want the yellow bit to be the same brightness at .5, or would I want it to be brighter (like 2 light sources overlapping)?

Screen itself is easy, it’s screen(a,b)=1-(1-a)(1-b) (again, per channel),

In the case of RGB pixels, Red and Green, with no overlap, I’m not sure it would do what I say above… you’d end up with R at .5 and G at .5 so Yellow at .5 on both, which might not look as bright as expected? Gamma correction?

Sounds like I’m gonna have to write some library code :slight_smile:

1 Like

I have a few handy, seen in folk’s patterns or ported from plain JS:


//linear interpolation
function lerp(a, b, t) {
  return a * (1-t) + b * t
}

//cross fade RGB between 2 colors using linear interpolation
//output sets r,g,b globals
function crossFadeColors(r1, g1, b1, r2, g2, b2, t) {
  r = lerp(r1, r2, t)
  g = lerp(g1, g2, t)
  b = lerp(b1, b2, t)
}

//convert HSV to RGB
//output sets r,g,b globals
function hsv2rgb(hh, ss, vv) {
  var h = mod(hh, 1)
  var s = clamp(ss, 0, 1)
  var v = clamp(vv, 0, 1)
  var i = floor(h * 6)
  var f = h * 6 - i
  var p = v * (1 - s)
  var q = v * (1 - (s * f))
  var 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
  }
}

//convert HSL to RGB
//output sets r,g,b globals
function hsl2rgb(h,s,l){
  value = s * min(l,1-l) + l
  sat2 = value ? 2-(2*l/value) : 0
  hsv2rgb(h,sat2,value);
}

//convert RGB to HSV
//output sets h,s,v globals
function rgb2hsv(r, g, b) {
  var rr, gg, bb, diff

  r = clamp(r, 0, 1)
  g = clamp(g, 0, 1)
  b = clamp(b, 0, 1)

  v = max(r, max(g, b))
  diff = v - min(r, min(g, b))
  if (diff == 0) {
    h = s = 0
  } else {
    s = diff / v
    rr = (v - r) / 6 / diff
    gg = (v - g) / 6 / diff
    bb = (v - b) / 6 / diff

    if (r == v) {
      h = bb - gg
    } else if (g == v) {
      h = (1 / 3) + rr - bb
    } else if (b == v) {
      h = (2 / 3) + gg - rr
    }
    if (h < 0) {
      h += 1
    } else if (h > 1) {
      h -= 1
    }
  }
}

(some could use slight tweaks/optimizations)

I’m considering these for inclusion in the API, but I want to figure out a good way to handle color “types”, and obviously wouldn’t be setting globals as a way of returning values.

1 Like

Ah ha, a missing piece

https://www.daveeddy.com/2014/07/01/red-yellow-and-blue/

The above links to JS code that implements RYB
(Red Yellow Blue) to RGB (and vice versa).

Implemented as RYB Color Wheel

RYB is what people expect (I mix red and yellow paint and get orange, yellow and blue to get green, red and blue to get purple…)

Time to write some more library code. Imagine a display that has dripping R Y B dots that mix to form colors.

Yes yes, these are RGB lights, they don’t work that way … But we can dream in paint. (Ref. What Dreams May Come)

Updated
I ended up optimizing/reducing the code (got rid of the matrix array), but the cubic interpolating (“biased (non-linear) interpolation”) is a killer. In the example below, it’s 17fps vs 87fps, all due to the math.
Granted, I am also converting 0…1 (angle, and to a limited extent, brightness) into 3 0…1 values (for R Y B). Warning - I heavily optimized, reducing places where I could see results would be zeros to actual zeros, rather than do the math.

Updated - see v2 on RYB colors act like paint

Obsolete code - see v2 - RYB color wheel compared to RGB/HSV on the PB
export var wheeltype, brightness

export function sliderWheelType(v){
  wheeltype = floor(v+.5)
}

export function sliderBrightness(v){
  brightness = v
}

export function beforeRender(delta) {
 t1 = time(.15)
 resetTransform()
 translate(-.5, -.5)
 rotate(t1 * PI2)
}

export function render2D(index,x,y) {
  a = mod(atan2(y,x)/PI2,1)
  if (wheeltype == 0) {
    rybwheel(a)
  } else {
    hsvwheel(a)
  }
}

function hsvwheel(a) {
    hsv(a, 1, brightness)
}

function rybwheel(a){
  v = clamp(brightness,.5,1)
  i = floor(a * 6)
  f = a * 6 - i
  q = v * (1 - f)
  t = v * f

  if (i == 0) {
    r_ = v; g_ = t; b_ = 0
  } else if (i == 1) {
    r_ = q; g_ = v; b_ = 0
  } else if (i == 2) {
    r_ = 0; g_ = v; b_ = t
  } else if (i == 3) {
    r_ = 0; g_ = q; b_ = v
  } else if (i == 4) {
    r_ = t; g_ = 0; b_ = v
  } else if (i == 5) {
    r_ = v; g_ = 0; b_ = q
  }
  
  ryb(r_,g_,b_)
}

function cubicInt(t, A, B){
  return A + (t * t * (3 - 2 * t)) * (B - A);
}

function getR(iR, iY, iB) {  // red
  x0 = cubicInt(iB, 1, .163);
  x1 = cubicInt(iB, 1, 0);
  x2 = cubicInt(iB, 1, .5);
  x3 = cubicInt(iB, 1, .2);
  y0 = cubicInt(iY, x0, x1);
  y1 = cubicInt(iY, x2, x3);
  return cubicInt(iR, y0, y1);
}

function getG(iR, iY, iB) {  // green
  x0 = cubicInt(iB, 1, .373);
  x1 = cubicInt(iB, 1, .66);
  x3 = cubicInt(iB, .5, .094);
  y0 = cubicInt(iY, x0, x1);
  y1 = cubicInt(iY, 0, x3);
  return cubicInt(iR, y0, y1);
}

function getB(iR, iY, iB) {  // blue
  x0 = cubicInt(iB, 1, .6);
  x1 = cubicInt(iB, 0, .2);
  x2 = cubicInt(iB, 0, .5);
  y0 = cubicInt(iY, x0, x1);
  y1 = cubicInt(iY, x2, 0);
  return cubicInt(iR, y0, y1);
}

function ryb(R,Y,B) {
      rgb(getR(R, Y, B), getG(R, Y, B), getB(R, Y, B))
}

One more update:

Note to myself:

YUV is also interesting:

The Y′UV model defines a color space in terms of one luma component (Y′) and two chrominance components, called U (blue projection) and V (red projection) respectively.

Which means the color space is 2D, unlike HSV (hue is 1D, though pieces of the color change are in the SV), and RGB/RYB (3D).
Seems like worth playing with, as another interesting way to represent color, for neat patterns.

Also, just realized this thread didn’t link to
this blending thread with code

Ooh and one more color space: HSI
Good description and algo for conversion
https://www.vocal.com/video/rgb-and-hsvhsihsl-color-space-conversion/

A few more bits to save:
http://aleto.ch/color-mixer/

Code for RGBW conversion

More on HSI

If we have an array returned, that’s essentially “standard”, either 3 or 4 values (RGBA?)

In doing the RYB, I really wanted to do 8bits*3 and get one number to work (since we have 16.16), but I ended up just avoiding passing things around much.

If we have an array color, it might be nice to just have

color.r
color.g
color.b
color.h
color.s
color.v

And so on. Any chance of just extending the array functions?