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:
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
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.
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.
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.
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?