Skip to content

Commit

Permalink
fix(color-contrast): correctly compute background color for elements …
Browse files Browse the repository at this point in the history
…with opacity (#3944)

* fix(create-grid): correctly compute stack order for non-positioned stacking contexts

* chore: Refactor createStackingOrder (#3932)

* chore: Refactor createStackingOrder

* Remove magic numbers

* use treewalker nodeIndex

* no floatpoint precision errors

* pass nodeIndex to createGrid for shadowDom

* do it properly

* remove whitespace

* fix(color-contrast): correctly handle opacity on parent elements

* working

* idea finalized

* remove comments

* move function

* tests!

* fix comments

* fix stuff

---------

Co-authored-by: Wilco Fiers <[email protected]>
  • Loading branch information
straker and WilcoFiers authored Mar 22, 2023
1 parent 1913a9e commit c051fe8
Show file tree
Hide file tree
Showing 9 changed files with 695 additions and 133 deletions.
180 changes: 90 additions & 90 deletions lib/commons/color/color.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,7 @@
import standards from '../../standards';

/**
* Convert a CSS color value into a number
*/
function convertColorVal(colorFunc, value, index) {
if (/%$/.test(value)) {
//<percentage>
if (index === 3) {
// alpha
return parseFloat(value) / 100;
}
return (parseFloat(value) * 255) / 100;
}
if (colorFunc[index] === 'h') {
// hue
if (/turn$/.test(value)) {
return parseFloat(value) * 360;
}
if (/rad$/.test(value)) {
return parseFloat(value) * 57.3;
}
}
return parseFloat(value);
}

/**
* Convert HSL to RGB
*/
function hslToRgb([hue, saturation, lightness, alpha]) {
// Must be fractions of 1
saturation /= 255;
lightness /= 255;

const high = (1 - Math.abs(2 * lightness - 1)) * saturation;
const low = high * (1 - Math.abs(((hue / 60) % 2) - 1));
const base = lightness - high / 2;

let colors;
if (hue < 60) {
// red - yellow
colors = [high, low, 0];
} else if (hue < 120) {
// yellow - green
colors = [low, high, 0];
} else if (hue < 180) {
// green - cyan
colors = [0, high, low];
} else if (hue < 240) {
// cyan - blue
colors = [0, low, high];
} else if (hue < 300) {
// blue - purple
colors = [low, 0, high];
} else {
// purple - red
colors = [high, 0, low];
}

return colors
.map(color => {
return Math.round((color + base) * 255);
})
.concat(alpha);
}
const hexRegex = /^#[0-9a-f]{3,8}$/i;
const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i;

/**
* @class Color
Expand All @@ -72,18 +11,20 @@ function hslToRgb([hue, saturation, lightness, alpha]) {
* @param {number} blue
* @param {number} alpha
*/
function Color(red, green, blue, alpha = 1) {
/** @type {number} */
this.red = red;
export default class Color {
constructor(red, green, blue, alpha = 1) {
/** @type {number} */
this.red = red;

/** @type {number} */
this.green = green;
/** @type {number} */
this.green = green;

/** @type {number} */
this.blue = blue;
/** @type {number} */
this.blue = blue;

/** @type {number} */
this.alpha = alpha;
/** @type {number} */
this.alpha = alpha;
}

/**
* Provide the hex string value for the color
Expand All @@ -92,7 +33,7 @@ function Color(red, green, blue, alpha = 1) {
* @instance
* @return {string}
*/
this.toHexString = function toHexString() {
toHexString() {
var redString = Math.round(this.red).toString(16);
var greenString = Math.round(this.green).toString(16);
var blueString = Math.round(this.blue).toString(16);
Expand All @@ -102,23 +43,20 @@ function Color(red, green, blue, alpha = 1) {
(this.green > 15.5 ? greenString : '0' + greenString) +
(this.blue > 15.5 ? blueString : '0' + blueString)
);
};
}

this.toJSON = function toJSON() {
toJSON() {
const { red, green, blue, alpha } = this;
return { red, green, blue, alpha };
};

const hexRegex = /^#[0-9a-f]{3,8}$/i;
const colorFnRegex = /^((?:rgb|hsl)a?)\s*\(([^\)]*)\)/i;
}

/**
* Parse any valid color string and assign its values to "this"
* @method parseString
* @memberof axe.commons.color.Color
* @instance
*/
this.parseString = function parseString(colorString) {
parseString(colorString) {
// IE occasionally returns named colors instead of RGB(A) values
if (standards.cssColors[colorString] || colorString === 'transparent') {
const [red, green, blue] = standards.cssColors[colorString] || [0, 0, 0];
Expand All @@ -139,7 +77,7 @@ function Color(red, green, blue, alpha = 1) {
return this;
}
throw new Error(`Unable to parse color "${colorString}"`);
};
}

/**
* Set the color value based on a CSS RGB/RGBA string
Expand All @@ -149,7 +87,7 @@ function Color(red, green, blue, alpha = 1) {
* @instance
* @param {string} rgb The string value
*/
this.parseRgbString = function parseRgbString(colorString) {
parseRgbString(colorString) {
// IE can pass transparent as value instead of rgba
if (colorString === 'transparent') {
this.red = 0;
Expand All @@ -159,7 +97,7 @@ function Color(red, green, blue, alpha = 1) {
return;
}
this.parseColorFnString(colorString);
};
}

/**
* Set the color value based on a CSS RGB/RGBA string
Expand All @@ -169,7 +107,7 @@ function Color(red, green, blue, alpha = 1) {
* @instance
* @param {string} rgb The string value
*/
this.parseHexString = function parseHexString(colorString) {
parseHexString(colorString) {
if (!colorString.match(hexRegex) || [6, 8].includes(colorString.length)) {
return;
}
Expand All @@ -191,7 +129,7 @@ function Color(red, green, blue, alpha = 1) {
} else {
this.alpha = 1;
}
};
}

/**
* Set the color value based on a CSS RGB/RGBA string
Expand All @@ -201,7 +139,7 @@ function Color(red, green, blue, alpha = 1) {
* @instance
* @param {string} rgb The string value
*/
this.parseColorFnString = function parseColorFnString(colorString) {
parseColorFnString(colorString) {
const [, colorFunc, colorValStr] = colorString.match(colorFnRegex) || [];
if (!colorFunc || !colorValStr) {
return;
Expand All @@ -226,7 +164,7 @@ function Color(red, green, blue, alpha = 1) {
this.green = colorNums[1];
this.blue = colorNums[2];
this.alpha = typeof colorNums[3] === 'number' ? colorNums[3] : 1;
};
}

/**
* Get the relative luminance value
Expand All @@ -236,7 +174,7 @@ function Color(red, green, blue, alpha = 1) {
* @instance
* @return {number} The luminance value, ranges from 0 to 1
*/
this.getRelativeLuminance = function getRelativeLuminance() {
getRelativeLuminance() {
var rSRGB = this.red / 255;
var gSRGB = this.green / 255;
var bSRGB = this.blue / 255;
Expand All @@ -249,7 +187,69 @@ function Color(red, green, blue, alpha = 1) {
bSRGB <= 0.03928 ? bSRGB / 12.92 : Math.pow((bSRGB + 0.055) / 1.055, 2.4);

return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
}
}

/**
* Convert a CSS color value into a number
*/
function convertColorVal(colorFunc, value, index) {
if (/%$/.test(value)) {
//<percentage>
if (index === 3) {
// alpha
return parseFloat(value) / 100;
}
return (parseFloat(value) * 255) / 100;
}
if (colorFunc[index] === 'h') {
// hue
if (/turn$/.test(value)) {
return parseFloat(value) * 360;
}
if (/rad$/.test(value)) {
return parseFloat(value) * 57.3;
}
}
return parseFloat(value);
}

export default Color;
/**
* Convert HSL to RGB
*/
function hslToRgb([hue, saturation, lightness, alpha]) {
// Must be fractions of 1
saturation /= 255;
lightness /= 255;

const high = (1 - Math.abs(2 * lightness - 1)) * saturation;
const low = high * (1 - Math.abs(((hue / 60) % 2) - 1));
const base = lightness - high / 2;

let colors;
if (hue < 60) {
// red - yellow
colors = [high, low, 0];
} else if (hue < 120) {
// yellow - green
colors = [low, high, 0];
} else if (hue < 180) {
// green - cyan
colors = [0, high, low];
} else if (hue < 240) {
// cyan - blue
colors = [0, low, high];
} else if (hue < 300) {
// blue - purple
colors = [low, 0, high];
} else {
// purple - red
colors = [high, 0, low];
}

return colors
.map(color => {
return Math.round((color + base) * 255);
})
.concat(alpha);
}
36 changes: 20 additions & 16 deletions lib/commons/color/flatten-colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,37 +88,41 @@ function simpleAlphaCompositing(Cs, αs, Cb, αb, blendMode) {
* @method flattenColors
* @memberof axe.commons.color.Color
* @instance
* @param {Color} fgColor Foreground color
* @param {Color} bgColor Background color
* @param {Color} sourceColor Foreground color
* @param {Color} backdrop Background color
* @return {Color} Blended color
*/
function flattenColors(fgColor, bgColor, blendMode = 'normal') {
function flattenColors(sourceColor, backdrop, blendMode = 'normal') {
// foreground is the "source" color and background is the "backdrop" color
const r = simpleAlphaCompositing(
fgColor.red,
fgColor.alpha,
bgColor.red,
bgColor.alpha,
sourceColor.red,
sourceColor.alpha,
backdrop.red,
backdrop.alpha,
blendMode
);
const g = simpleAlphaCompositing(
fgColor.green,
fgColor.alpha,
bgColor.green,
bgColor.alpha,
sourceColor.green,
sourceColor.alpha,
backdrop.green,
backdrop.alpha,
blendMode
);
const b = simpleAlphaCompositing(
fgColor.blue,
fgColor.alpha,
bgColor.blue,
bgColor.alpha,
sourceColor.blue,
sourceColor.alpha,
backdrop.blue,
backdrop.alpha,
blendMode
);

// formula: αo = αs + αb x (1 - αs)
// clamp alpha between 0 and 1
const αo = clamp(fgColor.alpha + bgColor.alpha * (1 - fgColor.alpha), 0, 1);
const αo = clamp(
sourceColor.alpha + backdrop.alpha * (1 - sourceColor.alpha),
0,
1
);
if (αo === 0) {
return new Color(r, g, b, αo);
}
Expand Down
Loading

0 comments on commit c051fe8

Please sign in to comment.