Skip to content

Commit

Permalink
fix(color-contrast): support color blend modes hue, saturation, color…
Browse files Browse the repository at this point in the history
…, luminosity (#4365)

Decided to add functions to the `Color` class itself to handle adding,
dividing, and multiplying a color by a value. Made it cleaner than
trying to do that in functions of the flatten color code.

Also refactored the code a bit to put the default export at the top.

Closes: #4170
  • Loading branch information
straker committed Mar 13, 2024
1 parent de1baa9 commit 7ae4761
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 30 deletions.
93 changes: 65 additions & 28 deletions lib/commons/color/flatten-colors.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Color from './color';

// clamp a value between two numbers (inclusive)
function clamp(value, min, max) {
return Math.min(Math.max(min, value), max);
}
// @see https://www.w3.org/TR/compositing-1/#blendingnonseparable
const nonSeparableBlendModes = ['hue', 'saturation', 'color', 'luminosity'];

// how to combine background and foreground colors together when using
// the CSS property `mix-blend-mode`. Defaults to `normal`
Expand Down Expand Up @@ -61,28 +59,32 @@ const blendFunctions = {
exclusion(Cb, Cs) {
// @see https://www.w3.org/TR/compositing-1/#blendingexclusion
return Cb + Cs - 2 * Cb * Cs;
},

// non-separate color function take the entire color object
// and not individual color components (red, green, blue)
hue(Cb, Cs) {
// @see https://www.w3.org/TR/compositing-1/#blendinghue
return Cs.setSaturation(Cb.getSaturation()).setLuminosity(
Cb.getLuminosity()
);
},
saturation(Cb, Cs) {
// @see https://www.w3.org/TR/compositing-1/#blendingsaturation
return Cb.setSaturation(Cs.getSaturation()).setLuminosity(
Cb.getLuminosity()
);
},
color(Cb, Cs) {
// @see https://www.w3.org/TR/compositing-1/#blendingcolor
return Cs.setLuminosity(Cb.getLuminosity());
},
luminosity(Cb, Cs) {
// @see https://www.w3.org/TR/compositing-1/#blendingluminosity
return Cb.setLuminosity(Cs.getLuminosity());
}
};

// Simple Alpha Compositing written as non-premultiplied.
// formula: Rrgb × Ra = Srgb × Sa + Drgb × Da × (1 − Sa)
// Cs: the source color
// αs: the source alpha
// Cb: the backdrop color
// αb: the backdrop alpha
// @see https://www.w3.org/TR/compositing-1/#simplealphacompositing
// @see https://www.w3.org/TR/compositing-1/#blending
// @see https://ciechanow.ski/alpha-compositing/
function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) {
return (
αs * (1 - αb) * Cs +
// Note: Cs and Cb values need to be between 0 and 1 inclusive for the blend function
// @see https://www.w3.org/TR/compositing-1/#simplealphacompositing
αs * αb * blendFunctions[blendMode](Cb / 255, Cs / 255) * 255 +
(1 - αs) * αb * Cb
);
}

/**
* Combine the two given color according to alpha blending.
* @method flattenColors
Expand All @@ -92,28 +94,35 @@ function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) {
* @param {Color} backdrop Background color
* @return {Color} Blended color
*/
function flattenColors(sourceColor, backdrop, blendMode = 'normal') {
export default function flattenColors(
sourceColor,
backdrop,
blendMode = 'normal'
) {
const blendingResult = blend(backdrop, sourceColor, blendMode);

// foreground is the "source" color and background is the "backdrop" color
const r = simpleAlphaCompositing(
sourceColor.red,
sourceColor.alpha,
backdrop.red,
backdrop.alpha,
blendMode
// we don't want to round the blended value
blendingResult.r * 255
);
const g = simpleAlphaCompositing(
sourceColor.green,
sourceColor.alpha,
backdrop.green,
backdrop.alpha,
blendMode
blendingResult.g * 255
);
const b = simpleAlphaCompositing(
sourceColor.blue,
sourceColor.alpha,
backdrop.blue,
backdrop.alpha,
blendMode
blendingResult.b * 255
);

// formula: αo = αs + αb x (1 - αs)
Expand Down Expand Up @@ -143,4 +152,32 @@ function flattenColors(sourceColor, backdrop, blendMode = 'normal') {
return new Color(Cr, Cg, Cb, αo);
}

export default flattenColors;
// Simple Alpha Compositing written as non-premultiplied.
// formula: Rrgb × Ra = Srgb × Sa + Drgb × Da × (1 − Sa)
// Cs: the source color
// αs: the source alpha
// Cb: the backdrop color
// αb: the backdrop alpha
// @see https://www.w3.org/TR/compositing-1/#simplealphacompositing
// @see https://www.w3.org/TR/compositing-1/#blending
// @see https://ciechanow.ski/alpha-compositing/
function simpleAlphaCompositing(Cs, αs, Cb, αb, blendingResult) {
return αs * (1 - αb) * Cs + αs * αb * blendingResult + (1 - αs) * αb * Cb;
}

// clamp a value between two numbers (inclusive)
function clamp(value, min, max) {
return Math.min(Math.max(min, value), max);
}

function blend(Cb, Cs, blendMode) {
if (nonSeparableBlendModes.includes(blendMode)) {
return blendFunctions[blendMode](Cb, Cs);
}

const C = new Color();
['r', 'g', 'b'].forEach(channel => {
C[channel] = blendFunctions[blendMode](Cb[channel], Cs[channel]);
});
return C;
}
84 changes: 84 additions & 0 deletions test/commons/color/flatten-colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,88 @@ describe('color.flattenColors ', function () {
assert.equal(flattenTwo.blue, 180);
assert.equal(flattenTwo.alpha, 1);
});

it('should flatten colors correctly using blend mode: hue', function () {
var flatten = axe.commons.color.flattenColors(colourTwo, colourOne, 'hue');
assert.equal(flatten.red, 162);
assert.equal(flatten.green, 50);
assert.equal(flatten.blue, 17);
assert.equal(flatten.alpha, 1);

var flattenTwo = axe.commons.color.flattenColors(
colourFour,
colourThree,
'hue'
);
assert.equal(flattenTwo.red, 188);
assert.equal(flattenTwo.green, 177);
assert.equal(flattenTwo.blue, 162);
assert.equal(flattenTwo.alpha, 1);
});

it('should flatten colors correctly using blend mode: saturation', function () {
var flatten = axe.commons.color.flattenColors(
colourTwo,
colourOne,
'saturation'
);
assert.equal(flatten.red, 185);
assert.equal(flatten.green, 35);
assert.equal(flatten.blue, 35);
assert.equal(flatten.alpha, 1);

var flattenTwo = axe.commons.color.flattenColors(
colourFour,
colourThree,
'saturation'
);
assert.equal(flattenTwo.red, 233);
assert.equal(flattenTwo.green, 151);
assert.equal(flattenTwo.blue, 181);
assert.equal(flattenTwo.alpha, 1);
});

it('should flatten colors correctly using blend mode: color', function () {
var flatten = axe.commons.color.flattenColors(
colourTwo,
colourOne,
'color'
);
assert.equal(flatten.red, 180);
assert.equal(flatten.green, 38);
assert.equal(flatten.blue, 34);
assert.equal(flatten.alpha, 1);

var flattenTwo = axe.commons.color.flattenColors(
colourFour,
colourThree,
'color'
);
assert.equal(flattenTwo.red, 161);
assert.equal(flattenTwo.green, 204);
assert.equal(flattenTwo.blue, 90);
assert.equal(flattenTwo.alpha, 1);
});

it('should flatten colors correctly using blend mode: luminosity', function () {
var flatten = axe.commons.color.flattenColors(
colourTwo,
colourOne,
'luminosity'
);
assert.equal(flatten.red, 226);
assert.equal(flatten.green, 33);
assert.equal(flatten.blue, 33);
assert.equal(flatten.alpha, 1);

var flattenTwo = axe.commons.color.flattenColors(
colourFour,
colourThree,
'luminosity'
);
assert.equal(flattenTwo.red, 214);
assert.equal(flattenTwo.green, 165);
assert.equal(flattenTwo.blue, 183);
assert.equal(flattenTwo.alpha, 1);
});
});
48 changes: 46 additions & 2 deletions test/integration/full/contrast/blending.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,47 @@ describe('color-contrast blending test', () => {
'rgb(255, 0, 77)',
'rgb(165, 176, 81)',
'rgb(150, 157, 119)',
'rgb(198, 198, 198)'
'rgb(198, 198, 198)',
// hue
'rgb(212, 212, 196)',
'rgb(255, 255, 255)',
'rgb(255, 255, 255)',
'rgb(125, 32, 54)',
'rgb(179, 39, 0)',
'rgb(195, 16, 77)',
'rgb(147, 180, 84)',
'rgb(150, 156, 117)',
'rgb(221, 221, 221)',
// saturation
'rgb(168, 239, 168)',
'rgb(255, 255, 255)',
'rgb(255, 255, 255)',
'rgb(169, 5, 76)',
'rgb(228, 11, 11)',
'rgb(255, 0, 0)',
'rgb(165, 171, 81)',
'rgb(150, 157, 112)',
'rgb(221, 221, 221)',
// color
'rgb(223, 207, 191)',
'rgb(255, 255, 255)',
'rgb(255, 255, 255)',
'rgb(125, 32, 54)',
'rgb(179, 39, 0)',
'rgb(195, 16, 77)',
'rgb(144, 182, 81)',
'rgb(150, 156, 120)',
'rgb(221, 221, 221)',
// luminosity
'rgb(124, 156, 124)',
'rgb(166, 166, 166)',
'rgb(210, 210, 210)',
'rgb(183, 4, 81)',
'rgb(254, 0, 0)',
'rgb(207, 0, 0)',
'rgb(171, 177, 87)',
'rgb(148, 154, 112)',
'rgb(221, 221, 221)'
];

const fixture = document.querySelector('#fixture');
Expand All @@ -137,7 +177,11 @@ describe('color-contrast blending test', () => {
'hard-light',
'soft-light',
'difference',
'exclusion'
'exclusion',
'hue',
'saturation',
'color',
'luminosity'
].forEach(blendMode => {
const nodes = testGroup.cloneNode(true);
const group = testGroup.cloneNode();
Expand Down

0 comments on commit 7ae4761

Please sign in to comment.