From a79b653331a9603f2616529a19591819a71fe76c Mon Sep 17 00:00:00 2001 From: FerrinThreatt Date: Fri, 27 Jun 2025 11:37:48 -0400 Subject: [PATCH 1/6] contrast function first PR --- src/color/p5.Color.js | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index d258bf10e2..4aa4a6bf81 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -28,6 +28,7 @@ import { P3 } from 'colorjs.io/fn'; + import HSBSpace from './color_spaces/hsb.js'; const map = (n, start1, stop1, start2, stop2, clamp) => { @@ -41,6 +42,17 @@ const map = (n, start1, stop1, start2, stop2, clamp) => { const serializationMap = {}; +const SRGB_THRESHOLD = 0.03928; +const SRGB_DIVISOR = 12.92; +const GAMMA_OFFSET = 0.055; +const GAMMA_DIVISOR = 1.055; +const GAMMA_EXPONENT = 2.4; +const LUMINANCE_RED = 0.2126; +const LUMINANCE_GREEN = 0.7152; +const LUMINANCE_BLUE = 0.0722; +const EPSILON = 0.05; +const THRESHOLD = 4.5; + class Color { // Reference to underlying color object depending on implementation // Not meant to be used publicly unless the implementation is known for sure @@ -291,6 +303,58 @@ class Color { return colorString; } +/** + * Checks if two colors contrast ratio is WCAG 2.1 compliant and returns the ratio + * + * @param {Color} other + * @returns {{ ratio: Number, passes: boolean }} + * @example + *
+ * + * + * function setup() { + * // Define colors + * let color1 = color(255, 255, 255); + * let color2 = color(0); + * + * // Test for contrast + * let result = color1.contrast(color2) + * + * console.log(result) + * + * } + * + *
+ */ + + contrast(new_other) { + //Constants for contrast ratio cutoffs + const CONTRAST_AA_NORMAL = 4.5; + const CONTRAST_AA_LARGE = 3.0; + const CONTRAST_AAA_NORMAL = 7.0; + const CONTRAST_AAA_LARGE = 4.5; + + //helper function for luminance aka brightness + let luminance = (c) => { + //putting RGB values into array and convert colors to value between 0-1 + let rgb = [red(c), green(c), blue(c)].map(v => { + v /= 255; + return v <= SRGB_THRESHOLD + ? v / SRGB_DIVISOR + : Math.pow((v + GAMMA_OFFSET) / GAMMA_DIVISOR, GAMMA_EXPONENT); + }); + return LUMINANCE_RED * rgb[0] + LUMINANCE_GREEN * rgb[1] + LUMINANCE_BLUE * rgb[2]; + }; + + let l1 = luminance(this); + let l2 = luminance(new_other); + //Epsilon to avoid dividing by zero + let ratio = (Math.max(l1, l2) + EPSILON) / (Math.min(l1, l2) + EPSILON); + + return { ratio, pass: ratio >= THRESHOLD }; + + }; + /** * Sets the red component of a color. * From 5fe2c719f789904e9951b8808f05ba2df5b9035d Mon Sep 17 00:00:00 2001 From: FerrinThreatt Date: Fri, 27 Jun 2025 13:39:45 -0400 Subject: [PATCH 2/6] contrast function and AA/AAA constants --- src/color/p5.Color.js | 48 ++++++++----------------------------------- src/core/constants.js | 15 ++++++++++++++ 2 files changed, 23 insertions(+), 40 deletions(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 4aa4a6bf81..185a703aeb 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -8,6 +8,7 @@ import { RGB, RGBHDR, HSL, HSB, HWB, LAB, LCH, OKLAB, OKLCH } from './creating_reading'; + import { ColorSpace, to, @@ -25,10 +26,9 @@ import { OKLab, OKLCH as OKLCHSpace, - + contrast, P3 } from 'colorjs.io/fn'; - import HSBSpace from './color_spaces/hsb.js'; const map = (n, start1, stop1, start2, stop2, clamp) => { @@ -42,16 +42,8 @@ const map = (n, start1, stop1, start2, stop2, clamp) => { const serializationMap = {}; -const SRGB_THRESHOLD = 0.03928; -const SRGB_DIVISOR = 12.92; -const GAMMA_OFFSET = 0.055; -const GAMMA_DIVISOR = 1.055; -const GAMMA_EXPONENT = 2.4; -const LUMINANCE_RED = 0.2126; -const LUMINANCE_GREEN = 0.7152; -const LUMINANCE_BLUE = 0.0722; -const EPSILON = 0.05; -const THRESHOLD = 4.5; + + class Color { // Reference to underlying color object depending on implementation @@ -302,7 +294,6 @@ class Color { } return colorString; } - /** * Checks if two colors contrast ratio is WCAG 2.1 compliant and returns the ratio * @@ -326,35 +317,12 @@ class Color { * * */ - - contrast(new_other) { - //Constants for contrast ratio cutoffs - const CONTRAST_AA_NORMAL = 4.5; - const CONTRAST_AA_LARGE = 3.0; - const CONTRAST_AAA_NORMAL = 7.0; - const CONTRAST_AAA_LARGE = 4.5; - - //helper function for luminance aka brightness - let luminance = (c) => { - //putting RGB values into array and convert colors to value between 0-1 - let rgb = [red(c), green(c), blue(c)].map(v => { - v /= 255; - return v <= SRGB_THRESHOLD - ? v / SRGB_DIVISOR - : Math.pow((v + GAMMA_OFFSET) / GAMMA_DIVISOR, GAMMA_EXPONENT); - }); - return LUMINANCE_RED * rgb[0] + LUMINANCE_GREEN * rgb[1] + LUMINANCE_BLUE * rgb[2]; + contrast(new_other) { + const contrast_method = 'WCAG21'; + const ratio = contrast(this._color, new_other._color, contrast_method); + return ratio; }; - let l1 = luminance(this); - let l2 = luminance(new_other); - //Epsilon to avoid dividing by zero - let ratio = (Math.max(l1, l2) + EPSILON) / (Math.min(l1, l2) + EPSILON); - - return { ratio, pass: ratio >= THRESHOLD }; - - }; - /** * Sets the red component of a color. * diff --git a/src/core/constants.js b/src/core/constants.js index 942b48c9ad..c51bae9306 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1371,3 +1371,18 @@ export const EXCLUDE = Symbol('exclude'); * @private */ export const JOIN = Symbol('join'); + +/** + * @typedef {'color-contrast-threshold-aa'} COLOR_CONTRAST_THRESHOLD_AA + * @property {COLOR_CONTRAST_THRESHOLD_AA} COLOR_CONTRAST_THRESHOLD_AA + * @final + */ +export const COLOR_CONTRAST_THRESHOLD_AA = 4.5; + + +/** + * @typedef {'color-contrast-threshold-aaa'} COLOR_CONTRAST_THRESHOLD_AAA + * @property {COLOR_CONTRAST_THRESHOLD_AAA} COLOR_CONTRAST_THRESHOLD_AAA + * @final + */ +export const COLOR_CONTRAST_THRESHOLD_AAA = 7.0; From beef1154162d7c03ea8b309cf8e3eeb496beed34 Mon Sep 17 00:00:00 2001 From: ksen0 Date: Tue, 14 Oct 2025 23:32:27 +0200 Subject: [PATCH 3/6] color.contrast implementation with example --- src/color/p5.Color.js | 86 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 185a703aeb..40bd10377f 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -295,33 +295,91 @@ class Color { return colorString; } /** - * Checks if two colors contrast ratio is WCAG 2.1 compliant and returns the ratio + * Checks the contrast between two colors, to make sure that they + * are different enough to be readable. The result of this function is + * a number that can be compared to `COLOR_CONTRAST_THRESHOLD_AA`, + * or to `COLOR_CONTRAST_THRESHOLD_AAA`. Shapes and UI elements should have color + * contrast at least `COLOR_CONTRAST_THRESHOLD_AA`, and text + * benefits from higher contrast of at least `COLOR_CONTRAST_THRESHOLD_AAA`. + * + * You can also explore this + * contrast checker tool. + * The contrast function in p5.js uses the WCAG 2.1 method the + * color.js contrast + * utility. + * + * * @param {Color} other - * @returns {{ ratio: Number, passes: boolean }} + * @returns {{ ratio: Number }} * @example *
* * + * // The contrast checker can be used both during development + * // with `print()`, or to help select readable colors on the fly. + * // This example shows both uses. + * + * let bgColor; + * let fg1Color; + * let fg2Color; + * * function setup() { - * // Define colors - * let color1 = color(255, 255, 255); - * let color2 = color(0); - * - * // Test for contrast - * let result = color1.contrast(color2) + * createCanvas(100, 100); + * bgColor = color(0); + * fg1Color = color(120); + * fg2Color = color(255); + * + * describe('A small square canvas with acentered text outlined by a thick stroke. The text reads 'click again!'. On every mouse click, the background, square outline, and text colors randomize, with high enough contrast for readability.'); + * } * - * console.log(result) + * function draw() { + * background(bgColor); + * stroke(fg1Color); + * noFill(); + * strokeWeight(5); + * rect(10, 10, 80, 80); * + * noStroke(); + * fill(fg2Color); + * textAlign(CENTER, CENTER); + * textSize(20); + * text("click\nagain!", 50, 50); + * } + * + * function mouseClicked(){ + * let newBgColor; + * let newFg1Color; + * let newFg2Color; + * + * // The loop may go for a long time, but it will not go on forever + * // It will stop the first time that the random colors contrast enough + * for (let i = 0; i < 10000; i += 1){ + * newBgColor = color(random(255), random(255), random(255)); + * newFg1Color = color(random(255), random(255), random(255)); + * newFg2Color = color(random(255), random(255), random(255)); + * if ( + * newBgColor.contrast(newFg2Color) > COLOR_CONTRAST_THRESHOLD_AAA && + * newBgColor.contrast(newFg1Color) > COLOR_CONTRAST_THRESHOLD_AA && + * newBgColor.contrast(newFg1Color) < COLOR_CONTRAST_THRESHOLD_AAA ){ + * + * bgColor = newBgColor; + * fg1Color = newFg1Color; + * fg2Color = newFg2Color; + * + * break; + * } + * } + * + * print("Contrast (rect)", bgColor.contrast(fg1Color)); + * print("Contrast (text)", bgColor.contrast(fg2Color)); * } * *
*/ - contrast(new_other) { - const contrast_method = 'WCAG21'; - const ratio = contrast(this._color, new_other._color, contrast_method); - return ratio; - }; + contrast(other_color) { + return contrastWCAG21(this._color, other_color._color); + }; /** * Sets the red component of a color. From 0e269b4d336bdda83ecf89f6acb916287d6c2a14 Mon Sep 17 00:00:00 2001 From: ksen0 Date: Tue, 14 Oct 2025 23:51:23 +0200 Subject: [PATCH 4/6] Added color contrast minimum constants documentation --- src/color/p5.Color.js | 27 +++++++++++++++++---------- src/core/constants.js | 16 ++++++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 40bd10377f..c2caa7967b 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -26,7 +26,7 @@ import { OKLab, OKLCH as OKLCHSpace, - contrast, + contrastWCAG21, P3 } from 'colorjs.io/fn'; import HSBSpace from './color_spaces/hsb.js'; @@ -294,15 +294,22 @@ class Color { } return colorString; } -/** + + /** * Checks the contrast between two colors, to make sure that they * are different enough to be readable. The result of this function is - * a number that can be compared to `COLOR_CONTRAST_THRESHOLD_AA`, - * or to `COLOR_CONTRAST_THRESHOLD_AAA`. Shapes and UI elements should have color - * contrast at least `COLOR_CONTRAST_THRESHOLD_AA`, and text - * benefits from higher contrast of at least `COLOR_CONTRAST_THRESHOLD_AAA`. + * a color contrast ratio that can be compared to `COLOR_CONTRAST_MINIMUM_GRAPHICS`, + * or to `COLOR_CONTRAST_MINIMUM_TEXT`. The higher the ratio, the more + * different colors are, and the more legible to a user. + * + * Graphics, interface elements, and large text should have a color + * contrast ratio of at least 4.5 (`COLOR_CONTRAST_MINIMUM_GRAPHICS`) + * + * Smaller text - less than at least 14 point or 19 pixels - + * should have a color contrast ratio of at least 7 + * (`COLOR_CONTRAST_MINIMUM_TEXT`) * - * You can also explore this + * The constants are based on WCAG AAA recommendations, which you can also explore in this * contrast checker tool. * The contrast function in p5.js uses the WCAG 2.1 method the * color.js contrast @@ -359,9 +366,9 @@ class Color { * newFg1Color = color(random(255), random(255), random(255)); * newFg2Color = color(random(255), random(255), random(255)); * if ( - * newBgColor.contrast(newFg2Color) > COLOR_CONTRAST_THRESHOLD_AAA && - * newBgColor.contrast(newFg1Color) > COLOR_CONTRAST_THRESHOLD_AA && - * newBgColor.contrast(newFg1Color) < COLOR_CONTRAST_THRESHOLD_AAA ){ + * newBgColor.contrast(newFg2Color) >= COLOR_CONTRAST_MINIMUM_TEXT && + * newBgColor.contrast(newFg1Color) >= COLOR_CONTRAST_MINIMUM_GRAPHICS && + * newBgColor.contrast(newFg1Color) < COLOR_CONTRAST_MINIMUM_TEXT ){ * * bgColor = newBgColor; * fg1Color = newFg1Color; diff --git a/src/core/constants.js b/src/core/constants.js index c51bae9306..d42ed5ab72 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1373,16 +1373,20 @@ export const EXCLUDE = Symbol('exclude'); export const JOIN = Symbol('join'); /** - * @typedef {'color-contrast-threshold-aa'} COLOR_CONTRAST_THRESHOLD_AA - * @property {COLOR_CONTRAST_THRESHOLD_AA} COLOR_CONTRAST_THRESHOLD_AA + * Can be used with `Color.contrast` to check if graphics, UI elements, and large text + * have enough contrast. + * @typedef {'color-contrast-minimum-graphics'} COLOR_CONTRAST_MINIMUM_GRAPHICS + * @property {COLOR_CONTRAST_MINIMUM_GRAPHICS} COLOR_CONTRAST_MINIMUM_GRAPHICS * @final */ -export const COLOR_CONTRAST_THRESHOLD_AA = 4.5; +export const COLOR_CONTRAST_MINIMUM_GRAPHICS = 4.5; /** - * @typedef {'color-contrast-threshold-aaa'} COLOR_CONTRAST_THRESHOLD_AAA - * @property {COLOR_CONTRAST_THRESHOLD_AAA} COLOR_CONTRAST_THRESHOLD_AAA + * Can be used with `Color.contrast` to check if text smaller than 14pt (~19px) + * has enough contrast. + * @typedef {'color-contrast-minimum-text'} COLOR_CONTRAST_MINIMUM_TEXT + * @property {COLOR_CONTRAST_MINIMUM_TEXT} COLOR_CONTRAST_MINIMUMD_TEXT * @final */ -export const COLOR_CONTRAST_THRESHOLD_AAA = 7.0; +export const COLOR_CONTRAST_MINIMUM_TEXT = 7.0; From aa00af857639582612ef583f0ee77cc636de7d45 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 23 Oct 2025 16:05:06 +0100 Subject: [PATCH 5/6] Implement a simpler API for contrast checking based on standards recommendation --- src/color/p5.Color.js | 175 ++++++++++++++++++++++++------------------ 1 file changed, 101 insertions(+), 74 deletions(-) diff --git a/src/color/p5.Color.js b/src/color/p5.Color.js index 63f5e0dc8f..b4440164c2 100644 --- a/src/color/p5.Color.js +++ b/src/color/p5.Color.js @@ -27,6 +27,7 @@ import { OKLab, OKLCH as OKLCHSpace, contrastWCAG21, + contrastAPCA, P3 } from 'colorjs.io/fn'; import HSBSpace from './color_spaces/hsb.js'; @@ -331,96 +332,122 @@ class Color { } /** - * Checks the contrast between two colors, to make sure that they - * are different enough to be readable. The result of this function is - * a color contrast ratio that can be compared to `COLOR_CONTRAST_MINIMUM_GRAPHICS`, - * or to `COLOR_CONTRAST_MINIMUM_TEXT`. The higher the ratio, the more - * different colors are, and the more legible to a user. - * - * Graphics, interface elements, and large text should have a color - * contrast ratio of at least 4.5 (`COLOR_CONTRAST_MINIMUM_GRAPHICS`) - * - * Smaller text - less than at least 14 point or 19 pixels - - * should have a color contrast ratio of at least 7 - * (`COLOR_CONTRAST_MINIMUM_TEXT`) - * - * The constants are based on WCAG AAA recommendations, which you can also explore in this - * contrast checker tool. - * The contrast function in p5.js uses the WCAG 2.1 method the - * color.js contrast - * utility. - * - + * Checks the contrast between two colors. This method returns a boolean + * value to indicate if the two color has enough contrast. `true` means that + * the colors has enough contrast to be used as background color and body + * text color. `false` means there is not enough contrast. + * + * A second argument can be passed to the method, `options` , which defines + * the algorithm to be used. The algorithms currently supported are + * WCAG 2.1 (`'WCAG21'`) or APCA (`'APCA'`). The default is WCAG 2.1. If a + * value of `'all'` is passed to the `options` argument, an object containing + * more details is returned. The details object will include the calculated + * contrast value of the colors and different passing criteria. + * + * For more details about color contrast, you can checkout this page from + * color.js. The WebAIM color contrast checker is a good tool to check out as + * well. * * @param {Color} other - * @returns {{ ratio: Number }} + * @returns {boolean|object} * @example *
* - * - * // The contrast checker can be used both during development - * // with `print()`, or to help select readable colors on the fly. - * // This example shows both uses. - * - * let bgColor; - * let fg1Color; - * let fg2Color; - * + * let bgColor, fg1Color, fg2Color, msg1, msg2; * function setup() { * createCanvas(100, 100); * bgColor = color(0); - * fg1Color = color(120); - * fg2Color = color(255); - * - * describe('A small square canvas with acentered text outlined by a thick stroke. The text reads 'click again!'. On every mouse click, the background, square outline, and text colors randomize, with high enough contrast for readability.'); + * fg1Color = color(100); + * fg2Color = color(220); + * + * if(bgColor.contrast(fg1Color)){ + * msg1 = 'good'; + * }else{ + * msg1 = 'bad'; + * } + * + * if(bgColor.contrast(fg2Color)){ + * msg2 = 'good'; + * }else{ + * msg2 = 'bad'; + * } + * + * describe('A black canvas with a faint grey word saying "bad" at the top left and a brighter light grey word saying "good" in the middle of the canvas.'); * } - * - * function draw() { + * + * function draw(){ * background(bgColor); - * stroke(fg1Color); - * noFill(); - * strokeWeight(5); - * rect(10, 10, 80, 80); - * - * noStroke(); + * + * textSize(18); + * + * fill(fg1Color); + * text(msg1, 10, 30); + * * fill(fg2Color); - * textAlign(CENTER, CENTER); - * textSize(20); - * text("click\nagain!", 50, 50); + * text(msg2, 10, 60); * } - * - * function mouseClicked(){ - * let newBgColor; - * let newFg1Color; - * let newFg2Color; - * - * // The loop may go for a long time, but it will not go on forever - * // It will stop the first time that the random colors contrast enough - * for (let i = 0; i < 10000; i += 1){ - * newBgColor = color(random(255), random(255), random(255)); - * newFg1Color = color(random(255), random(255), random(255)); - * newFg2Color = color(random(255), random(255), random(255)); - * if ( - * newBgColor.contrast(newFg2Color) >= COLOR_CONTRAST_MINIMUM_TEXT && - * newBgColor.contrast(newFg1Color) >= COLOR_CONTRAST_MINIMUM_GRAPHICS && - * newBgColor.contrast(newFg1Color) < COLOR_CONTRAST_MINIMUM_TEXT ){ - * - * bgColor = newBgColor; - * fg1Color = newFg1Color; - * fg2Color = newFg2Color; - * - * break; - * } - * } - * - * print("Contrast (rect)", bgColor.contrast(fg1Color)); - * print("Contrast (text)", bgColor.contrast(fg2Color)); + * + *
+ * + *
+ * + * let bgColor, fgColor, contrast; + * function setup() { + * createCanvas(100, 100); + * bgColor = color(0); + * fgColor = color(200); + * contrast = bgColor.contrast(fgColor, 'all'); + * + * describe('A black canvas with four short lines of grey text that respectively says: "WCAG 2.1", "12.55", "APCA", and "-73.30".'); + * } + * + * function draw(){ + * background(bgColor); + * + * textSize(14); + * + * fill(fgColor); + * text('WCAG 2.1', 10, 25); + * text(nf(contrast.WCAG21.value, 0, 2), 10, 40); + * + * text('APCA', 10, 70); + * text(nf(contrast.APCA.value, 0, 2), 10, 85); * } * *
*/ - contrast(other_color) { - return contrastWCAG21(this._color, other_color._color); + contrast(other_color, options='WCAG21') { + if(options !== 'all'){ + let contrastVal, minimum; + switch(options){ + case 'WCAG21': + contrastVal = contrastWCAG21(this._color, other_color._color); + minimum = 4.5; + break; + case 'APCA': + contrastVal = Math.abs(contrastAPCA(this._color, other_color._color)); + minimum = 75; + break; + default: + return null; + } + + return contrastVal >= minimum; + }else{ + const wcag21Value = contrastWCAG21(this._color, other_color._color); + const apcaValue = contrastAPCA(this._color, other_color._color); + return { + WCAG21: { + value: wcag21Value, + passedMinimum: wcag21Value >= 4.5, + passedAAA: wcag21Value >= 7 + }, + APCA: { + value: apcaValue, + passedMinimum: Math.abs(apcaValue) >= 75 + } + }; + } }; /** From 1c5e4c4f3d5bfa7d52cc876423602ab705822a25 Mon Sep 17 00:00:00 2001 From: limzykenneth Date: Thu, 23 Oct 2025 16:14:45 +0100 Subject: [PATCH 6/6] Remove unused contrast constants --- src/core/constants.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/core/constants.js b/src/core/constants.js index 947eb79220..c203106730 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -1294,22 +1294,3 @@ export const EXCLUDE = Symbol('exclude'); * @private */ export const JOIN = Symbol('join'); - -/** - * Can be used with `Color.contrast` to check if graphics, UI elements, and large text - * have enough contrast. - * @typedef {'color-contrast-minimum-graphics'} COLOR_CONTRAST_MINIMUM_GRAPHICS - * @property {COLOR_CONTRAST_MINIMUM_GRAPHICS} COLOR_CONTRAST_MINIMUM_GRAPHICS - * @final - */ -export const COLOR_CONTRAST_MINIMUM_GRAPHICS = 4.5; - - -/** - * Can be used with `Color.contrast` to check if text smaller than 14pt (~19px) - * has enough contrast. - * @typedef {'color-contrast-minimum-text'} COLOR_CONTRAST_MINIMUM_TEXT - * @property {COLOR_CONTRAST_MINIMUM_TEXT} COLOR_CONTRAST_MINIMUMD_TEXT - * @final - */ -export const COLOR_CONTRAST_MINIMUM_TEXT = 7.0;