diff --git a/lib/commons/color/flatten-colors.js b/lib/commons/color/flatten-colors.js index 34c96c82e4..9d17e5f228 100644 --- a/lib/commons/color/flatten-colors.js +++ b/lib/commons/color/flatten-colors.js @@ -65,19 +65,23 @@ const blendFunctions = { // an not individual color components (red, green, blue) hue(Cb, Cs) { // @see https://www.w3.org/TR/compositing-1/#blendinghue - return setLuminosity(setSaturation(Cs, saturation(Cb)), luminosity(Cb)); + return Cs.setSaturation(Cb.getSaturation()).setLuminosity( + Cb.getLuminosity() + ); }, saturation(Cb, Cs) { // @see https://www.w3.org/TR/compositing-1/#blendingsaturation - return setLuminosity(setSaturation(Cb, saturation(Cs)), luminosity(Cb)); + return Cb.setSaturation(Cs.getSaturation()).setLuminosity( + Cb.getLuminosity() + ); }, color(Cb, Cs) { // @see https://www.w3.org/TR/compositing-1/#blendingcolor - return setLuminosity(Cs, luminosity(Cb)); + return Cs.setLuminosity(Cb.getLuminosity()); }, luminosity(Cb, Cs) { // @see https://www.w3.org/TR/compositing-1/#blendingluminosity - return setLuminosity(Cb, luminosity(Cs)); + return Cb.setLuminosity(Cs.getLuminosity()); } }; @@ -95,15 +99,7 @@ export default function flattenColors( backdrop, blendMode = 'normal' ) { - let blendingResult; - if (nonSeparableBlendModes.includes(blendMode)) { - // 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 - const Cs = sourceColor.divide(255); - const Cb = backdrop.divide(255); - - blendingResult = blendFunctions[blendMode](Cb, Cs).multiply(255); - } + const blendingResult = blend(backdrop, sourceColor, blendMode); // foreground is the "source" color and background is the "backdrop" color const r = simpleAlphaCompositing( @@ -111,24 +107,22 @@ export default function flattenColors( sourceColor.alpha, backdrop.red, backdrop.alpha, - blendMode, - blendingResult?.red + // 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?.green + blendingResult.g * 255 ); const b = simpleAlphaCompositing( sourceColor.blue, sourceColor.alpha, backdrop.blue, backdrop.alpha, - blendMode, - blendingResult?.blue + blendingResult.b * 255 ); // formula: αo = αs + αb x (1 - αs) @@ -151,11 +145,11 @@ export default function flattenColors( // // RGB color space doesn't have decimal values so we will follow what browsers do and round // e.g. rgb(255.2, 127.5, 127.8) === rgb(255, 128, 128) - const Cred = Math.round(r / αo); - const Cgreen = Math.round(g / αo); - const Cblue = Math.round(b / αo); + const Cr = Math.round(r / αo); + const Cg = Math.round(g / αo); + const Cb = Math.round(b / αo); - return new Color(Cred, Cgreen, Cblue, αo); + return new Color(Cr, Cg, Cb, αo); } // Simple Alpha Compositing written as non-premultiplied. @@ -167,17 +161,8 @@ export default function flattenColors( // @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, blendingResult) { - return ( - αs * (1 - αb) * Cs + - αs * - αb * - (blendingResult ?? - // 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 - blendFunctions[blendMode](Cb / 255, Cs / 255) * 255) + - (1 - αs) * αb * Cb - ); +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) @@ -185,74 +170,14 @@ function clamp(value, min, max) { return Math.min(Math.max(min, value), max); } -// following functions taken from the spec -// @see https://www.w3.org/TR/compositing-1/#blendingnonseparable -function luminosity(color) { - return 0.3 * color.red + 0.59 * color.green + 0.11 * color.blue; -} - -function clipColor(color) { - const L = luminosity(color); - const n = Math.min(color.red, color.green, color.blue); - const x = Math.max(color.red, color.green, color.blue); - - if (n < 0) { - return new Color( - L + ((color.red - L) * L) / (L - n), - L + ((color.green - L) * L) / (L - n), - L + ((color.blue - L) * L) / (L - n), - color.alpha - ); - } - - if (x > 1) { - return new Color( - L + ((color.red - L) * (1 - L)) / (x - L), - L + ((color.green - L) * (1 - L)) / (x - L), - L + ((color.blue - L) * (1 - L)) / (x - L), - color.alpha - ); +function blend(Cb, Cs, blendMode) { + if (nonSeparableBlendModes.includes(blendMode)) { + return blendFunctions[blendMode](Cb, Cs); } - return color; -} - -function setLuminosity(color, L) { - const d = L - luminosity(color); - return clipColor(color.add(d)); -} - -function saturation(color) { - return ( - Math.max(color.red, color.green, color.blue) - - Math.min(color.red, color.green, color.blue) - ); -} - -function setSaturation(color, s) { - const C = new Color(color.red, color.green, color.blue, color.alpha); - const colorEntires = Object.entries(C) - .filter(([prop]) => prop !== 'alpha') - .map(([name, value]) => { - return { name, value }; - }); - - // find the min, mid, and max values of the color components - const [Cmin, Cmid, Cmax] = colorEntires.sort((a, b) => { - return a.value - b.value; + const C = new Color(); + ['r', 'g', 'b'].forEach(channel => { + C[channel] = blendFunctions[blendMode](Cb[channel], Cs[channel]); }); - - if (Cmax.value > Cmin.value) { - Cmid.value = ((Cmid.value - Cmin.value) * s) / (Cmax.value - Cmin.value); - Cmax.value = s; - } else { - Cmid.value = Cmax.value = 0; - } - - Cmin.value = 0; - - C[Cmax.name] = Cmax.value; - C[Cmin.name] = Cmin.value; - C[Cmid.name] = Cmid.value; return C; }