diff --git a/lib/commons/color/flatten-colors.js b/lib/commons/color/flatten-colors.js index 9ac19f0d11..e70ceafd33 100644 --- a/lib/commons/color/flatten-colors.js +++ b/lib/commons/color/flatten-colors.js @@ -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` @@ -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 @@ -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) @@ -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; +} diff --git a/test/commons/color/flatten-colors.js b/test/commons/color/flatten-colors.js index 45c575f59a..0d152616bc 100644 --- a/test/commons/color/flatten-colors.js +++ b/test/commons/color/flatten-colors.js @@ -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); + }); }); diff --git a/test/integration/full/contrast/blending.js b/test/integration/full/contrast/blending.js index 1a75035a56..b103be987f 100644 --- a/test/integration/full/contrast/blending.js +++ b/test/integration/full/contrast/blending.js @@ -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'); @@ -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();