Skip to content

Commit

Permalink
Output format (#78)
Browse files Browse the repository at this point in the history
* Added color format output functionality to contrast-colors package

* readme update

* readme update
  • Loading branch information
NateBaldwinDesign authored Aug 12, 2020
1 parent 4e518ed commit 72094f8
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 16 deletions.
18 changes: 17 additions & 1 deletion packages/contrast-colors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,23 @@ Optional value from 0-100 indicating the brightness of the base / background col
#### `contrast` *integer*:
Optional value to increase contrast of your generated colors. This value is multiplied against all ratios defined for each color scale.

#### Output
#### `output` *string (enum)*:
String value of the desired color space and output format for the generated colors. Output formats conform to the [W3C CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) spec for the supported options.

| Output option | Sample value |
|---------------|--------------|
| `'HEX'` _(default)_ | `#RRGGBB` |
| `'RGB'` | `rgb(255, 255, 255)` |
| `'HSL'` | `hsl(360deg, 0%, 100%)` |
| `'HSV'` | `hsv(360deg, 0%, 100%)` |
| `'HSLuv'` | `hsluv(360, 0, 100)` |
| `'LAB'` | `lab(100%, 0, 0)` |
| `'LCH'` | `lch(100%, 0, 360deg)` |
| `'CAM02'` | `jab(100%, 0, 0)`|
| `'CAM02p'` | `jch(100%, 0, 360deg)` |


#### Function outputs and examples
The `generateAdaptiveTheme` function returns an array of color objects. Each key is named by concatenating the user-defined color name (above) with a numeric value.

Colors with a **positive contrast ratio** with the base (ie, 2:1) will be named in increments of 100. For example, `gray100`, `gray200`.
Expand Down
159 changes: 144 additions & 15 deletions packages/contrast-colors/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,46 +112,60 @@ const colorSpaces = {
CAM02: {
name: 'jab',
channels: ['J', 'a', 'b'],
interpolator: d3.interpolateJab
interpolator: d3.interpolateJab,
function: d3.jab
},
CAM02p: {
name: 'jch',
channels: ['J', 'C', 'h'],
interpolator: d3.interpolateJch
interpolator: d3.interpolateJch,
function: d3.jch
},
LCH: {
name: 'hcl',
name: 'lch', // named per correct color definition order
channels: ['h', 'c', 'l'],
interpolator: d3.interpolateHcl,
white: d3.hcl(NaN, 0, 100),
black: d3.hcl(NaN, 0, 0)
black: d3.hcl(NaN, 0, 0),
function: d3.hcl
},
LAB: {
name: 'lab',
channels: ['l', 'a', 'b'],
interpolator: d3.interpolateLab
interpolator: d3.interpolateLab,
function: d3.lab
},
HSL: {
name: 'hsl',
channels: ['h', 's', 'l'],
interpolator: d3.interpolateHsl
interpolator: d3.interpolateHsl,
function: d3.hsl
},
HSLuv: {
name: 'hsluv',
channels: ['l', 'u', 'v'],
interpolator: d3.interpolateHsluv,
white: d3.hsluv(NaN, NaN, 100),
black: d3.hsluv(NaN, NaN, 0)
black: d3.hsluv(NaN, NaN, 0),
function: d3.hsluv
},
RGB: {
name: 'rgb',
channels: ['r', 'g', 'b'],
interpolator: d3.interpolateRgb
interpolator: d3.interpolateRgb,
function: d3.rgb
},
HSV: {
name: 'hsv',
channels: ['h', 's', 'v'],
interpolator: d3.interpolateHsv
interpolator: d3.interpolateHsv,
function: d3.hsv
},
HEX: {
name: 'hex',
channels: ['r', 'g', 'b'],
interpolator: d3.interpolateRgb,
function: d3.rgb
}
};

Expand Down Expand Up @@ -309,7 +323,8 @@ function generateContrastColors({
base,
ratios,
colorspace = 'LAB',
smooth = false
smooth = false,
output = 'HEX'
} = {}) {
if (!base) {
throw new Error(`Base is undefined`);
Expand All @@ -328,6 +343,10 @@ function generateContrastColors({
if (!ratios) {
throw new Error(`Ratios are undefined`);
}
const outputFormat = colorSpaces[output];
if (!outputFormat) {
throw new Error(`Colorspace “${output}” not supported`);
}

let swatches = 3000;

Expand All @@ -350,12 +369,114 @@ function generateContrastColors({
// Return color matching target ratio, or closest number
for (let i=0; i < ratios.length; i++){
let r = binarySearch(contrasts, ratios[i], baseV);
newColors.push(d3.rgb(scaleData.colors[r]).hex());

// use fixColorValue function to convert each color to the specified
// output format.
newColors.push(fixColorValue(scaleData.colors[r], output));

}

return newColors;
}

// Helper function to change any NaN to a zero
function filterNaN(x) {
if(isNaN(x)) {
return 0;
} else {
return x;
}
}

// Helper function for rounding color values to whole numbers
function fixColorValue(color, format, object = false) {
let colorObj = colorSpaces[format].function(color);
let propArray = colorSpaces[format].channels;

let newColorObj = {
[propArray[0]]: filterNaN(colorObj[propArray[0]]),
[propArray[1]]: filterNaN(colorObj[propArray[1]]),
[propArray[2]]: filterNaN(colorObj[propArray[2]])
}

// HSLuv
if (format === "HSLuv") {
for (let i = 0; i < propArray.length; i++) {

let roundedPct = Math.round(newColorObj[propArray[i]]);
newColorObj[propArray[i]] = roundedPct;
}
}
// LAB, LCH, JAB, JCH
else if (format === "LAB" || format === "LCH" || format === "CAM02" || format === "CAM02p") {
for (let i = 0; i < propArray.length; i++) {
let roundedPct = Math.round(newColorObj[propArray[i]]);

if (propArray[i] === "h" && !object) {
roundedPct = roundedPct + "deg";
}
if (propArray[i] === "l" && !object || propArray[i] === "J" && !object) {
roundedPct = roundedPct + "%";
}

newColorObj[propArray[i]] = roundedPct;

}
}
else {
for (let i = 0; i < propArray.length; i++) {
if (propArray[i] === "s" || propArray[i] === "l" || propArray[i] === "v") {
// leave as decimal format
let roundedPct = parseFloat(newColorObj[propArray[i]].toFixed(2));
if(object) {
newColorObj[propArray[i]] = roundedPct;
}
else {
newColorObj[propArray[i]] = Math.round(roundedPct * 100) + "%";
}
}
else {
let roundedPct = parseFloat(newColorObj[propArray[i]].toFixed());
if (propArray[i] === "h" && !object) {
roundedPct = roundedPct + "deg";
}
newColorObj[propArray[i]] = roundedPct;
}
}
}

let stringName = colorSpaces[format].name;
let stringValue;

if (format === "HEX") {
stringValue = d3.rgb(color).formatHex();
} else {
let str0, srt1, str2;
if (format === "LCH") {
// Have to force opposite direction of array index for LCH
// because d3 defines the channel order as "h, c, l" but we
// want the output to be in the correct format
str0 = newColorObj[propArray[2]] + ", ";
str1 = newColorObj[propArray[1]] + ", ";
str2 = newColorObj[propArray[0]];
}
else {
str0 = newColorObj[propArray[0]] + ", ";
str1 = newColorObj[propArray[1]] + ", ";
str2 = newColorObj[propArray[2]];
}

stringValue = stringName + "(" + str0 + str1 + str2 + ")";
}

if (object) {
// return colorObj;
return newColorObj;
} else {
return stringValue;
}
}

function luminance(r, g, b) {
let a = [r, g, b].map((v) => {
v /= 255;
Expand Down Expand Up @@ -431,7 +552,13 @@ function ratioName(r) {
return nArr;
}

function generateAdaptiveTheme({colorScales, baseScale, brightness, contrast = 1}) {
function generateAdaptiveTheme({
colorScales,
baseScale,
brightness,
contrast = 1,
output = 'HEX'
}) {
if (!baseScale) {
throw new Error('baseScale is undefined');
}
Expand Down Expand Up @@ -464,7 +591,7 @@ function generateAdaptiveTheme({colorScales, baseScale, brightness, contrast = 1

if (brightness === undefined) {
return function(brightness, contrast) {
return generateAdaptiveTheme({baseScale: baseScale, colorScales: colorScales, brightness: brightness, contrast: contrast});
return generateAdaptiveTheme({baseScale: baseScale, colorScales: colorScales, brightness: brightness, contrast: contrast, output: output});
}
}
else {
Expand Down Expand Up @@ -527,7 +654,8 @@ function generateAdaptiveTheme({colorScales, baseScale, brightness, contrast = 1
colorspace: colorScales[i].colorspace,
ratios: ratios,
base: bval,
smooth: smooth
smooth: smooth,
output: output
});

for (let i=0; i < outputColors.length; i++) {
Expand Down Expand Up @@ -605,5 +733,6 @@ module.exports = {
generateContrastColors,
minPositive,
ratioName,
generateAdaptiveTheme
generateAdaptiveTheme,
fixColorValue
};
Loading

0 comments on commit 72094f8

Please sign in to comment.