Skip to content

Commit aa00af8

Browse files
committed
Implement a simpler API for contrast checking based on standards recommendation
1 parent 2a8a13e commit aa00af8

File tree

1 file changed

+101
-74
lines changed

1 file changed

+101
-74
lines changed

src/color/p5.Color.js

Lines changed: 101 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
OKLab,
2828
OKLCH as OKLCHSpace,
2929
contrastWCAG21,
30+
contrastAPCA,
3031
P3
3132
} from 'colorjs.io/fn';
3233
import HSBSpace from './color_spaces/hsb.js';
@@ -331,96 +332,122 @@ class Color {
331332
}
332333

333334
/**
334-
* Checks the contrast between two colors, to make sure that they
335-
* are different enough to be readable. The result of this function is
336-
* a color contrast ratio that can be compared to `COLOR_CONTRAST_MINIMUM_GRAPHICS`,
337-
* or to `COLOR_CONTRAST_MINIMUM_TEXT`. The higher the ratio, the more
338-
* different colors are, and the more legible to a user.
339-
*
340-
* Graphics, interface elements, and large text should have a color
341-
* contrast ratio of at least 4.5 (`COLOR_CONTRAST_MINIMUM_GRAPHICS`)
342-
*
343-
* Smaller text - less than at least 14 point or 19 pixels -
344-
* should have a color contrast ratio of at least 7
345-
* (`COLOR_CONTRAST_MINIMUM_TEXT`)
346-
*
347-
* The constants are based on WCAG AAA recommendations, which you can also explore in this
348-
* <a href="https://webaim.org/resources/contrastchecker/">contrast checker tool</a>.
349-
* The contrast function in p5.js uses the WCAG 2.1 method the
350-
* <a href="https://colorjs.io/docs/contrast.html">color.js contrast</a>
351-
* utility.
352-
*
353-
335+
* Checks the contrast between two colors. This method returns a boolean
336+
* value to indicate if the two color has enough contrast. `true` means that
337+
* the colors has enough contrast to be used as background color and body
338+
* text color. `false` means there is not enough contrast.
339+
*
340+
* A second argument can be passed to the method, `options` , which defines
341+
* the algorithm to be used. The algorithms currently supported are
342+
* WCAG 2.1 (`'WCAG21'`) or APCA (`'APCA'`). The default is WCAG 2.1. If a
343+
* value of `'all'` is passed to the `options` argument, an object containing
344+
* more details is returned. The details object will include the calculated
345+
* contrast value of the colors and different passing criteria.
346+
*
347+
* For more details about color contrast, you can checkout this page from
348+
* color.js. The WebAIM color contrast checker is a good tool to check out as
349+
* well.
354350
*
355351
* @param {Color} other
356-
* @returns {{ ratio: Number }}
352+
* @returns {boolean|object}
357353
* @example
358354
* <div>
359355
* <code>
360-
*
361-
* // The contrast checker can be used both during development
362-
* // with `print()`, or to help select readable colors on the fly.
363-
* // This example shows both uses.
364-
*
365-
* let bgColor;
366-
* let fg1Color;
367-
* let fg2Color;
368-
*
356+
* let bgColor, fg1Color, fg2Color, msg1, msg2;
369357
* function setup() {
370358
* createCanvas(100, 100);
371359
* bgColor = color(0);
372-
* fg1Color = color(120);
373-
* fg2Color = color(255);
374-
*
375-
* 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.');
360+
* fg1Color = color(100);
361+
* fg2Color = color(220);
362+
*
363+
* if(bgColor.contrast(fg1Color)){
364+
* msg1 = 'good';
365+
* }else{
366+
* msg1 = 'bad';
367+
* }
368+
*
369+
* if(bgColor.contrast(fg2Color)){
370+
* msg2 = 'good';
371+
* }else{
372+
* msg2 = 'bad';
373+
* }
374+
*
375+
* 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.');
376376
* }
377-
*
378-
* function draw() {
377+
*
378+
* function draw(){
379379
* background(bgColor);
380-
* stroke(fg1Color);
381-
* noFill();
382-
* strokeWeight(5);
383-
* rect(10, 10, 80, 80);
384-
*
385-
* noStroke();
380+
*
381+
* textSize(18);
382+
*
383+
* fill(fg1Color);
384+
* text(msg1, 10, 30);
385+
*
386386
* fill(fg2Color);
387-
* textAlign(CENTER, CENTER);
388-
* textSize(20);
389-
* text("click\nagain!", 50, 50);
387+
* text(msg2, 10, 60);
390388
* }
391-
*
392-
* function mouseClicked(){
393-
* let newBgColor;
394-
* let newFg1Color;
395-
* let newFg2Color;
396-
*
397-
* // The loop may go for a long time, but it will not go on forever
398-
* // It will stop the first time that the random colors contrast enough
399-
* for (let i = 0; i < 10000; i += 1){
400-
* newBgColor = color(random(255), random(255), random(255));
401-
* newFg1Color = color(random(255), random(255), random(255));
402-
* newFg2Color = color(random(255), random(255), random(255));
403-
* if (
404-
* newBgColor.contrast(newFg2Color) >= COLOR_CONTRAST_MINIMUM_TEXT &&
405-
* newBgColor.contrast(newFg1Color) >= COLOR_CONTRAST_MINIMUM_GRAPHICS &&
406-
* newBgColor.contrast(newFg1Color) < COLOR_CONTRAST_MINIMUM_TEXT ){
407-
*
408-
* bgColor = newBgColor;
409-
* fg1Color = newFg1Color;
410-
* fg2Color = newFg2Color;
411-
*
412-
* break;
413-
* }
414-
* }
415-
*
416-
* print("Contrast (rect)", bgColor.contrast(fg1Color));
417-
* print("Contrast (text)", bgColor.contrast(fg2Color));
389+
* </code>
390+
* </div>
391+
*
392+
* <div>
393+
* <code>
394+
* let bgColor, fgColor, contrast;
395+
* function setup() {
396+
* createCanvas(100, 100);
397+
* bgColor = color(0);
398+
* fgColor = color(200);
399+
* contrast = bgColor.contrast(fgColor, 'all');
400+
*
401+
* describe('A black canvas with four short lines of grey text that respectively says: "WCAG 2.1", "12.55", "APCA", and "-73.30".');
402+
* }
403+
*
404+
* function draw(){
405+
* background(bgColor);
406+
*
407+
* textSize(14);
408+
*
409+
* fill(fgColor);
410+
* text('WCAG 2.1', 10, 25);
411+
* text(nf(contrast.WCAG21.value, 0, 2), 10, 40);
412+
*
413+
* text('APCA', 10, 70);
414+
* text(nf(contrast.APCA.value, 0, 2), 10, 85);
418415
* }
419416
* </code>
420417
* </div>
421418
*/
422-
contrast(other_color) {
423-
return contrastWCAG21(this._color, other_color._color);
419+
contrast(other_color, options='WCAG21') {
420+
if(options !== 'all'){
421+
let contrastVal, minimum;
422+
switch(options){
423+
case 'WCAG21':
424+
contrastVal = contrastWCAG21(this._color, other_color._color);
425+
minimum = 4.5;
426+
break;
427+
case 'APCA':
428+
contrastVal = Math.abs(contrastAPCA(this._color, other_color._color));
429+
minimum = 75;
430+
break;
431+
default:
432+
return null;
433+
}
434+
435+
return contrastVal >= minimum;
436+
}else{
437+
const wcag21Value = contrastWCAG21(this._color, other_color._color);
438+
const apcaValue = contrastAPCA(this._color, other_color._color);
439+
return {
440+
WCAG21: {
441+
value: wcag21Value,
442+
passedMinimum: wcag21Value >= 4.5,
443+
passedAAA: wcag21Value >= 7
444+
},
445+
APCA: {
446+
value: apcaValue,
447+
passedMinimum: Math.abs(apcaValue) >= 75
448+
}
449+
};
450+
}
424451
};
425452

426453
/**

0 commit comments

Comments
 (0)