-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RYB colors #207
Comments
The paper referenced here proposes an "intuitive color mixing" technique: use an RYB (Red-Yellow-Blue) subtractive model inspired by how physical pigments work. By expressing colors as RYB, you can perform simple compositing, presumably using the
As defined like this, it’s not clear how much of the corresponding RGB space can a RYB space contain, and in any case it’s defined as a one-way conversion, with no formula provided for decomposing RGB into the RYB components. RYB is therefore not a candidate for defining as color space in Culori. Similar issues make it challenging to add pigment-decomposition models inspired by Kubelka-Munk theory (e.g. mixbox) as color spaces. Let’s see, however, if it makes sense to add some helper functions to Culori to make RYB to RGB conversion more pleasant to implement. |
I’m thinking the following may be useful additions to the Culori API:
|
Added With this in place, the RYB to RGB method looks like this: import { trilerp } from 'culori';
const RYB_CUBE = [
{ mode: 'rgb', r: 1, g: 1, b: 1 }, // white
{ mode: 'rgb', r: 1, g: 0, b: 0 }, // red
{ mode: 'rgb', r: 1, g: 1, b: 0 }, // yellow
{ mode: 'rgb', r: 1, g: 0.5, b: 0 }, // orange
{ mode: 'rgb', r: 0.163, g: 0.373, b: 0.6 }, // blue
{ mode: 'rgb', r: 0.5, g: 0, b: 0.5 }, // violet
{ mode: 'rgb', r: 0, g: 0.66, b: 0.2 }, // green
{ mode: 'rgb', r: 0.2, g: 0.094, b: 0 } // black
];
function ryb2rgb(coords) {
const biased_coords = coords.map(t => t * t * (3 - 2 * t));
return {
mode: 'rgb',
r: trilerp(...RYB_CUBE.map(it => it.r), ...biased_coords),
g: trilerp(...RYB_CUBE.map(it => it.g), ...biased_coords),
b: trilerp(...RYB_CUBE.map(it => it.b), ...biased_coords)
};
}
ryb2rgb([1, 0.5, 0.25]);
/*
{
mode: 'rgb',
r: 0.8984375,
g: 0.21828124999999998,
b: 0.0390625
}
*/ By changing the colors at the corner of the cube you can perform a trilinear interpolation between any 8 colors. |
@danburzo thanks so much! |
@danburzo Itten would be proud :D https://codepen.io/meodai/pen/NWELdGW/7cbdbbcc1d1dd0ae42527341eef1c23c?editors=0010 PS: looks like the teal color is getting lost in the "mix" somehow. |
Yeah, the code is a crude approximation of real-world color mixing, but it looks pretty nice! |
@danburzo I think the code in your example inverts white & black (they need to be swapped in RYB_CUBE) |
RYB is a subtractive color model, so [1, 1, 1] produces black, not white (as with RGB/HSL). |
haha I should not late-night code :D thanks |
Just a note on this comment, the RYB space as Gosset and Chen describe can be translated both in the forward and reverse direction, even with biasing applied. Using Newton's method you can successfully implement a forward and reverse transform for all colors in gamut. If extrapolating outside of the gamut, things get a little dicey, but in gamut, it actually works quite well. >>> c1 = Color.random('ryb')
>>> c1
color(--ryb 0.98577 0.35263 0.92582 / 1)
>>> c1.convert('srgb')
color(srgb 0.43535 0.05045 0.30214 / 1)
>>> c1.convert('srgb').convert('ryb')
color(--ryb 0.98577 0.35263 0.92582 / 1) I think as a color space, biasing should not be the default as it just clumps all the colors toward the corners, but the approach works for both. >>> c2 = Color.random('ryb-biased')
>>> c2
color(--ryb-biased 0.13099 0.43229 0.29285 / 1)
>>> c2.convert('srgb')
color(srgb 0.81598 0.85969 0.54394 / 1)
>>> c2.convert('srgb').convert('ryb-biased')
color(--ryb-biased 0.13099 0.43229 0.29285 / 1) A reverse transform may not work for any 8 colors either. It is easy to turn the 3D space inside out. You can see how sRGB looks in the Gosset and Chen RYB color space. It gets quite twisted, but it still works. Through testing, I found that picking any colors for the 8 RYB corners may yield degraded round trip results near the limits in some cases, but for the Gosset and Chen colors, it works quite well. The actual implementation for inverse trilinear interpolation is straightforward, but no, I didn't come up with this part myself. Reference code for both reverse bilinear and trilinear interpolation is found here: https://stackoverflow.com/a/18332009/3609487. This approach seems to use this method of trilinear interpolation to calculate the inverse (http://paulbourke.net/miscellaneous/interpolation/) as it is easier to target and calculate the individual components in this way. As for the biasing, there was no reference, but I was able to just code up a simple Newton's method inverse as the easing function is simple to calculate the derivative from. You do need a fairly sound matrix inverse function as you can sometimes get zeros at the pivot points, but that does not necessarily mean the matrix is not invertible, so you need to shuffle the rows around when reducing them. If you do get a Jacobian matrix that is not invertible (only ever occurs outside the gamut), you can probably just bail, or that is what I did. I do not guarantee round trips outside of the RYB gamut. This may or may not be more work than this library cares to do for RYB, but I had been working on this off and on for a bit, and when I saw this, I figured I'd share. It helped motivate me to actually clean up the work and get it out into the wild. |
I appreciate you taking the time to write this up, Isaac! Very useful additions to the thread. Bourke’s variant of Playing some more with RYB, I noticed the easing function is in fact Smoothstep, so the code needed to add a import { trilerp, easingSmoothstep } from 'culori';
function convertRybToRgb(ryb) {
// Omit the call to easingSmoothstep() to remove bias.
const r = easingSmoothstep(ryb.r);
const y = easingSmoothstep(ryb.y);
const b = easingSmoothstep(ryb.b);
return {
mode: 'rgb',
r: trilerp(1, 1, 1, 1, .163, .5, 0, .2, r, y, b),
g: trilerp(1, 0, 1, .5, .373, 0, .66, .094, r, y, b),
b: trilerp(1, 0, 0, 0, .6, .5, .2, 0, r, y, b)
};
}
// Usage
convertRybToRgb({ mode: 'ryb', r:…, y:…, b:… }); It’s also nice that Smoothstep has an analytical inverse, to help in the RGB to RYB conversion: function easingInverseSmoothstep(t) {
return 0.5 - Math.sin(Math.asin(1 - 2 * t) / 3);
} Is the code to produce 3D gamut visualizations available somewhere I could take a closer look? |
Just for fun, a little RYB color picker: https://danburzo.ro/demos/color/ryb.html |
3D demos and others found here https://facelessuser.github.io/coloraide/demos/ |
3D demo is also available in the repo to do locally, but the browser demo is more accessible and doesn't require me to explain setup :). |
Whoops! I didn't enable RYB gamut in 3D demo I just updated it. Certain algorithms do have limits. For instance, doing RYB in other gamuts will just give you messed up models, so it only works well with its own gamut, but doing other spaces in the RYB gamut is kind of fun. Here's Jzazbz: https://facelessuser.github.io/coloraide/demos/3d_models.html?space=jzazbz&gamut=ryb&edges=false&aspect=false&ortho=false EDIT: Some spaces will refuse to render in other gamuts, for instance, HPLuv I only renders in its own gamut. I should probably do that for RYB as well as it is smaller than the other gamuts, so you can't resize the gamut shell to fit it. |
I do think it makes a bit more sense. I'll probably make that adjustment. |
That's actually the phrasing I use in my docs 🤦🏻 . |
Update made, thanks for the suggestion! |
I recently wanted to play with RYB colors. I experimented using some code of: https://github.com/bahamas10/ryb/blob/gh-pages/js/RXB.js the results are pretty. Would be so nice o have this in culori as well
(Found an other pen using it https://codepen.io/yukulele/pen/XMbrBJ?editors=0010)
The text was updated successfully, but these errors were encountered: