Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(color-contrast): correctly compute background color for elements with opacity #3944

Merged
merged 16 commits into from
Mar 22, 2023
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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this opportunity to do something I've been wanting to do for awhile now which is convert the Color constructor function into a true class. The advantage to this was that I was able to do deepEquals on two Color objects whereas before that was not possible (thus greatly simplifying the tests). Another advantage is that all Color objects now share memory for all their functions thanks to the prototype whereas before each Color object had their own memory instance for each function.

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