Skip to content

Commit

Permalink
feat: updated support for APCA with tests and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
NateBaldwinDesign committed Aug 9, 2022
1 parent d2fb1b6 commit 13251e5
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 48 deletions.
6 changes: 6 additions & 0 deletions packages/contrast-colors/test/contrast.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,10 @@ test('should provide APCA contrast of ~ 106', () => {
const contrastValue = contrast([0, 0, 0], [255,255,255], undefined, 'wcag3'); // lighter gray is UI color, gray is base. Should return negative whole number

expect(contrastValue).toBe(106.04067321268862);
});

test('should provide APCA contrast less than APCA officially supports', () => {
const contrastValue = contrast([238, 238, 238], [255,255,255], undefined, 'wcag3'); // Leonardo needs more than just 7.5+ for contrast values

expect(contrastValue).toBe(7.567424744881627);
});
2 changes: 1 addition & 1 deletion packages/contrast-colors/test/convertColorValue.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/

import { convertColorValue } from "../index.js";
import { convertColorValue } from "../index";

test('should return color object for HSL color', function() {
let result = convertColorValue('#2c66f1', 'HSL', true);
Expand Down
50 changes: 48 additions & 2 deletions packages/contrast-colors/test/theme.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,6 @@ test('should generate 2 colors with bidirectional contrast (dark background)', (
}); // positive & negative ratios
const theme = new Theme({ colors: [color], backgroundColor: '#323232' });
const themeColors = theme.contrastColorValues;
console.log(theme.contrastColors)

expect(themeColors).toEqual(['#121c4e', '#9895c0']);
});
Expand Down Expand Up @@ -1064,7 +1063,54 @@ test('should remove a color from existing theme', () => {
]);
});

// Expected errors
/**
* APCA contrast test
*/

test('should use APCA to generate theme for three colors', () => {
const gray = new BackgroundColor({ name: 'gray', colorKeys: ['#cacaca', '#323232'], colorspace: 'HSL', ratios: [8, 60, 75, 90, 106] });
const blue = new Color({ name: 'blue', colorKeys: ['#0000ff'], colorspace: 'LAB', ratios: [40, 60, 75, 90] });
const red = new Color({ name: 'red', colorKeys: ['#ff0000'], colorspace: 'RGB', ratios: [40, 60, 75, 90] });
const theme = new Theme({ colors: [gray, blue, red], backgroundColor: gray, lightness: 100, formula: 'wcag3' });
const themeColors = theme.contrastColors;

expect(themeColors).toEqual([
{ background: '#ffffff' },
{
name: 'gray',
values: [
{ name: 'gray100', contrast: 8, value: '#ededed' },
{ name: 'gray200', contrast: 60, value: '#8e8e8e' },
{ name: 'gray300', contrast: 75, value: '#6e6e6e' },
{ name: 'gray400', contrast: 90, value: '#4a4a4a' },
{ name: 'gray500', contrast: 106, value: '#000000' }
],
},
{
name: 'blue',
values: [
{ name: 'blue100', contrast: 40, value: '#c6a4ff' },
{ name: 'blue200', contrast: 60, value: '#9f73ff' },
{ name: 'blue300', contrast: 75, value: '#7145ff' },
{ name: 'blue400', contrast: 90, value: '#1a08dd' }
],
},
{
name: 'red',
values: [
{ name: 'red100', contrast: 40, value: '#ff9797' },
{ name: 'red200', contrast: 60, value: '#ff4444' },
{ name: 'red300', contrast: 75, value: '#d20000' },
{ name: 'red400', contrast: 90, value: '#850000' }
],
},
]);
});


/**
* Expected errors
*/
test('should generate no colors, missing colorKeys', () => {
expect(
() => {
Expand Down
21 changes: 20 additions & 1 deletion packages/contrast-colors/test/themeSetters.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,23 @@ test('should update predefined colors interpolation', () => {
const themeColors = theme.contrastColorValues;

expect(themeColors).toEqual(['#d86202', '#b84601']);
});
});

// // Formula setter
// test('should set formula to wcag3 with updated contrast values', () => {
// const color = new Color({ name: 'Color', colorKeys: ['#2451FF', '#C9FEFE', '#012676'], ratios: [3, 4.5], colorspace: 'CAM02' });
// const theme = new Theme({ colors: [color], backgroundColor: '#f5f5f5', output: 'HEX' });
// theme.formula = 'wcag3';
// const themeColors = theme.contrastColors;

// expect(themeColors).toEqual([
// { background: '#f5f5f5' },
// {
// name: 'Color',
// values: [
// { name: 'Color100', contrast: 60, value: '#548fe0' },
// { name: 'Color200', contrast: 75, value: '#2b66f0' }
// ]
// }
// ]);
// });
16 changes: 13 additions & 3 deletions packages/contrast-colors/theme.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { colorSpaces, convertColorValue, multiplyRatios, ratioName, round, searc
import { BackgroundColor } from "./backgroundcolor";

class Theme {
constructor({ colors, backgroundColor, lightness, contrast = 1, saturation = 100, output = 'HEX' }) {
constructor({ colors, backgroundColor, lightness, contrast = 1, saturation = 100, output = 'HEX', formula = 'wcag2' }) {
this._output = output;
this._colors = colors;
this._lightness = lightness;
this._saturation = saturation;
this._formula = formula;

this._setBackgroundColor(backgroundColor);
this._setBackgroundColorValue();
Expand All @@ -45,6 +46,15 @@ class Theme {
this._findContrastColorValues();
}

set formula(formula) {
this._formula = formula;
this._findContrastColors();
}

get formula() {
return this._formula;
}

set contrast(contrast) {
this._contrast = contrast;
this._findContrastColors();
Expand Down Expand Up @@ -239,12 +249,12 @@ class Theme {
// modify target ratio based on contrast multiplier
ratioValues = ratioValues.map((ratio) => multiplyRatios(+ratio, this._contrast));

const contrastColors = searchColors(color, bgRgbArray, baseV, ratioValues).map((clr) => convertColorValue(clr, this._output));
const contrastColors = searchColors(color, bgRgbArray, baseV, ratioValues, this._formula).map((clr) => convertColorValue(clr, this._output));

for (let i = 0; i < contrastColors.length; i++) {
let n;
if (!swatchNames) {
const rVal = ratioName(color.ratios)[i];
const rVal = ratioName(color.ratios, this._formula)[i];
n = color.name.concat(rVal).replace(/\s+/g, ''); // concat with ratio name and remove any spaces from original name
} else {
n = swatchNames[i];
Expand Down
13 changes: 7 additions & 6 deletions packages/contrast-colors/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -422,17 +422,18 @@ function getContrast(color, base, baseV, method='wcag2') {
}
}

function minPositive(r) {
function minPositive(r, formula) {
if (!r) { throw new Error('Array undefined'); }
if (!Array.isArray(r)) { throw new Error('Passed object is not an array'); }
return Math.min(...r.filter((val) => val >= 1));
const min = (formula === 'wcag2') ? 0 : 1;
return Math.min(...r.filter((val) => val >= min));
}

function ratioName(r) {
function ratioName(r, formula) {
if (!r) { throw new Error('Ratios undefined'); }
r = r.sort((a, b) => a - b); // sort ratio array in case unordered

const min = minPositive(r);
const min = minPositive(r, formula);
const minIndex = r.indexOf(min);
const nArr = []; // names array

Expand All @@ -455,7 +456,7 @@ function ratioName(r) {
return nArr;
}

const searchColors = (color, bgRgbArray, baseV, ratioValues) => {
const searchColors = (color, bgRgbArray, baseV, ratioValues, formula) => {
const colorLen = 3000;
const colorScale = createScale({
swatches: colorLen,
Expand All @@ -472,7 +473,7 @@ const searchColors = (color, bgRgbArray, baseV, ratioValues) => {
return ccache[i];
}
const rgb = chroma(colorScale(i)).rgb();
const c = getContrast(rgb, bgRgbArray, baseV);
const c = getContrast(rgb, bgRgbArray, baseV, formula);
ccache[i] = c;
// ccounter++;
return c;
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/js/addScaleBulkDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function addScaleBulk(e) {
// id is scaleType
let id = e.target.id.replace('_addBulk', '');

console.log(id)
// console.log(id)
let colorNameInputId = id.concat('_name')
let colorNameInput = document.getElementById(colorNameInputId);
let colorName = colorNameInput.value;
Expand Down Expand Up @@ -55,7 +55,7 @@ function cancelScaleBulk() {
function bulkScaleItemColorInput(e) {
let id = e.target.parentNode.parentNode.parentNode.id;
let itemId = id.replace('_dialog', '');
console.log(itemId)
// console.log(itemId)
const currentColor = (itemId === 'sequential') ? _sequentialScale : ((itemId === 'divergingScale') ? _divergingScale : _qualitativeScale);

const currentKeys = currentColor.colorKeys;
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/js/bulkConvertDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function bulkItemConvertColorInput(e) {
let bulkValues = bulkInputs.value.replace(/\r\n/g,"\n").replace(/[,\/]/g,"\n").replace(" ", "").replace(/['\/]/g, "").replace(/["\/]/g, "").replace(" ", "").split("\n");
bulkValues = bulkValues.map((value) => {return value.replace(" ", "")});
for (let i=0; i<bulkValues.length; i++) {
console.log(bulkValues[i])
// console.log(bulkValues[i])
if (!bulkValues[i].startsWith('#')) {
bulkValues[i] = '#' + bulkValues[i]
}
Expand All @@ -67,7 +67,7 @@ function bulkItemConvertColorInput(e) {
data.push(createColorJson(c, opts))
})

console.log(data)
// console.log(data)

const replacer = (key, value) => value === null ? '' : value // specify how you want to handle null values here
const header = Object.keys(data[0])
Expand All @@ -76,7 +76,7 @@ function bulkItemConvertColorInput(e) {
...data.map(row => header.map(fieldName => JSON.stringify(row[fieldName], replacer)).join(','))
].join('\r\n')

console.log(csv)
// console.log(csv)

const csvData = Promise.resolve(csv);

Expand Down
12 changes: 8 additions & 4 deletions packages/ui/src/js/createOutputColors.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
function createOutputColors(dest) {
if(dest) dest = document.getElementById(dest);

let wcagFormula = document.getElementById('themeWCAG').value;
let swatchesOutputs = document.getElementById('swatchesOutputs')
let themeOutputs = document.getElementById('themeOutputs');
swatchesOutputs.classList = 'hideSwatchLuminosity hideSwatchContrast';
Expand Down Expand Up @@ -88,11 +89,12 @@ function createOutputColors(dest) {
// get the ratio to print inside the swatch
let contrast = theme[i].values[j].contrast;
let colorArray = [d3.rgb(colorForTransform).r, d3.rgb(colorForTransform).g, d3.rgb(colorForTransform).b]
let actualContrast = Leo.contrast(colorArray, themeBackgroundColorArray);
let actualContrast = Leo.contrast(colorArray, themeBackgroundColorArray, undefined, wcagFormula);

let innerTextColor = (d3.hsluv(colorForTransform).v > 50) ? '#000000' : '#ffffff';
let contrastRounded = (Math.round(actualContrast * 100))/100;
let contrastText = document.createTextNode(contrastRounded + ' :1');
let contrastTextNode = (wcagFormula === 'wcag2') ? contrastRounded + ' :1' : contrastRounded;
let contrastText = document.createTextNode(contrastTextNode);
let contrastTextSpan = document.createElement('span');
contrastTextSpan.className = 'themeOutputSwatch_contrast';
contrastTextSpan.appendChild(contrastText);
Expand Down Expand Up @@ -184,6 +186,7 @@ function createOutputColors(dest) {
}

function createDetailOutputColors(colorName) {
let wcagFormula = document.getElementById('themeWCAG').value;
let swatchesOutputs = document.getElementById('detailSwatchesOutputs')
if(swatchesOutputs) swatchesOutputs.innerHTML = ' ';

Expand Down Expand Up @@ -244,11 +247,12 @@ function createDetailOutputColors(colorName) {
// get the ratio to print inside the swatch
let contrast = colorOutput.values[j].contrast;
let colorArray = [d3.rgb(colorForTransform).r, d3.rgb(colorForTransform).g, d3.rgb(colorForTransform).b]
let actualContrast = Leo.contrast(colorArray, themeBackgroundColorArray);
let actualContrast = Leo.contrast(colorArray, themeBackgroundColorArray, undefined, wcagFormula);

let innerTextColor = (d3.hsluv(colorForTransform).v > 50) ? '#000000' : '#ffffff';
let contrastRounded = (Math.round(actualContrast * 100))/100;
let contrastText = document.createTextNode(contrastRounded + ' :1');
let contrastTextNode = (wcagFormula === 'wcag2') ? contrastRounded + ' :1' : contrastRounded;
let contrastText = document.createTextNode(contrastTextNode);
let contrastTextSpan = document.createElement('span');
contrastTextSpan.className = 'themeOutputSwatch_contrast';
contrastTextSpan.appendChild(contrastText);
Expand Down
9 changes: 7 additions & 2 deletions packages/ui/src/js/createOutputParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ let ${themeName.replace(/\s/g, '')} = new Leo.Theme({
contrast: ${_theme.contrast},
saturation: ${_theme.saturation},
output: "${_theme.output}"
formula: "${_theme.formula}"
});`;

const highlightedCode = hljs.highlight(paramOutputString, {language: 'javascript'}).value
Expand Down Expand Up @@ -122,17 +123,21 @@ function createTokensOutput() {
description: `UI background color. All color contrasts evaluated and generated against this color.`
}
themeObj['Background'] = backgroundColorObj

let formulaString = (_theme.formula === 'wcag2') ? 'WCAG 2.x (relative luminance)' : 'WCAG 3 (APCA)';
let largeText = (_theme.formula === 'wcag3') ? 60 : 3;
let smallText = (_theme.formula === 'wcag3') ? 75 : 4.5;

for(let i=1; i < contrastColors.length; i++) {
let thisColor = contrastColors[i];
for(let j=0; j < thisColor.values.length; j++) {
let color = thisColor.values[j]
let descriptionText = (color.contrast < 3) ? textLowContrast : ((color.contrast >= 3 && color.contrast < 4.5) ? textLarge : textSmall);
let descriptionText = (color.contrast < largeText) ? textLowContrast : ((color.contrast >= largeText && color.contrast < smallText) ? textLarge : textSmall);

let colorObj = {
value: color.value,
type: "color",
description: `${descriptionText} Contrast is ${color.contrast}:1 against background ${backgroundColor}`
description: `${descriptionText} ${formulaString} contrast is ${color.contrast}:1 against background ${backgroundColor}`
}
themeObj[color.name] = colorObj
}
Expand Down
7 changes: 4 additions & 3 deletions packages/ui/src/js/createRatioChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ function createRatioChart(chartRatios, bool) {
let dest2 = document.getElementById('detailContrastChart');
if(dest2) dest2.innerHTML = ' ';

let wcagFormula = document.getElementById('themeWCAG').value;
let lightness = Number(document.getElementById('themeBrightnessSlider').value);
// Calculate highest possible contrast ratio (black or white) against background color
const maxPossibleRatio = (lightness > 50) ? Leo.contrast([0, 0, 0], chroma(_theme.contrastColors[0].background).rgb()): Leo.contrast([255, 255, 255], chroma(_theme.contrastColors[0].background).rgb());
const maxPossibleRatio = (lightness > 50) ? Leo.contrast([0, 0, 0], chroma(_theme.contrastColors[0].background).rgb(), undefined, wcagFormula) : Leo.contrast([255, 255, 255], chroma(_theme.contrastColors[0].background).rgb(), undefined, wcagFormula);

const fillRange = (start, end) => {
return Array(end - start + 1).fill().map((item, index) => start + index);
Expand All @@ -61,8 +62,8 @@ function createRatioChart(chartRatios, bool) {
}
];
let minRatio = Math.min(...chartRatios);
let yMin = (minRatio < 1) ? minRatio: 1;
let yMax = 21;
let yMin = (wcagFormula === 'wcag3') ? 0 : ( (minRatio < 1) ? minRatio: 1 );
let yMax = (wcagFormula === 'wcag3') ? 106 : 21;

createChart(dataContrast, 'Contrast ratio', 'Swatches', "#contrastChart", yMin, yMax, true, undefined, undefined, bool);
// for color details view
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/js/initialTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const tempGray = new Leo.BackgroundColor({
name: 'Gray',
colorKeys: ['#000000'],
colorspace: 'RGB',
ratios: [3, 4.5],
ratios: [3.2, 4.5],
output: 'HEX'
});

Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/js/params.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ function paramSetup() {
let hash = window.location.hash.toString();
// let newParam = hash.replaceAll(`#`, `%23`).replaceAll(`,`, `%54`);
let paramArray = hash.split('&');
console.log(paramArray)
// console.log(paramArray)
let paramOptions = ['base', 'mode', 'ratios'];
paramArray.map((p) => {
for(let i = 0; i < paramOptions.length; i++) {
Expand Down Expand Up @@ -171,7 +171,7 @@ function paramSetup() {
setTimeout(() => {
RATIOCOLORS = Promise.resolve(_theme.contrastColors[1].values.map((c) => {return c.value}));
RATIOCOLORS.then((resolve) => {
console.log(resolve)
// console.log(resolve)
addRatioInputs(RATIOS, resolve)
});
}, 100)
Expand All @@ -186,7 +186,7 @@ function paramSetup() {
addRatioInputs([
1.45,
2.05,
3,
3.02,
4.54,
7,
10.86
Expand Down
Loading

0 comments on commit 13251e5

Please sign in to comment.