Skip to content

Instantly share code, notes, and snippets.

@kurtroberts
Last active November 21, 2023 04:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kurtroberts/89b8e3904d2a19180d80b7fcb4954d33 to your computer and use it in GitHub Desktop.
Save kurtroberts/89b8e3904d2a19180d80b7fcb4954d33 to your computer and use it in GitHub Desktop.
Understanding the math behind blend modes

Understanding the math behind blend modes

This started as a REPL session to try to understand why our Goodbeast.com colors look so much better when a friendly designer used them than when I did (other than his exceptional sense of color). I realized that he was "overlaying" them on top of themselves, effectively increasing both brightness and saturation.

This all came together as a shader, forked to start and mostly credited. https://www.shadertoy.com/view/DtKyRc

The overlay function in the javascript version can be thought of more as an "S curve" in a design program - it increases intensity via a Sigmoid function. The one below is what Wikipedia believes Photoshop uses. Most of the rest of this is just utility functions for dealing with different types of color notation. I think the shader style vec3() notation is the easiest to do math with. Hence the convention of passing colors like {r, g, b}.

A real overlay function would take two different colors. This would be trivial, and is left to an exercise for the reader.

Try something like toHex(overlay(hex('#57166B'))) to get started.

var overlayChannel = (value) => {
// https://en.wikipedia.org/wiki/Blend_modes
if (value < .5) {
return 2.0 * value * value;
} else {
return 1.0 - 2.0 * (1.0 - value) * (1.0 - value);
}
},
overlayShader = (colorObj) => {
var { r, g, b } = colorObj;
return { r: overlayChannel(r), g: overlayChannel(g), b: overlayChannel(b) };
},
convertShaderToDecimal = (colorObj) => {
var { r, g, b } = colorObj;
//multiply by 255 and then take the integer portion, bitwise OR 0 does that
//fast - https://gist.github.com/kurtroberts/8d4d6718514b2f5d9e16
return { r: r * 255 | 0, g: g * 255 | 0, b: b * 255 | 0};
},
convertDecimalToShader = (colorObj) => {
var { r, g, b } = colorObj;
return { r: r / 255, g: g / 255, b: b / 255 };
},
overlay = (colorObj) => {
return convertShaderToDecimal(overlayShader(convertDecimalToShader(colorObj)));
},
//so you can copy and paste from browser console
rgb = (r, g, b) => {
return { r, g, b };
},
toRgb = (colorObj) => {
var { r, g, b } = colorObj;
return `rgb(${r}, ${g}, ${b})`;
},
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
hex = (hexcode) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexcode);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
},
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
componentToHex = (c) => {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
},
// https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
toHex = (colorObj) => {
var { r, g, b } = colorObj;
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
};
module.exports = {
overlayChannel,
overlay,
overlayShader,
convertDecimalToShader,
convertShaderToDecimal,
rgb,
hex,
toHex,
toRgb
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment