@adobe/leonardo-contrast-colors
Advanced tools
Comparing version 1.0.0-alpha.8 to 1.0.0-alpha.9
@@ -6,2 +6,10 @@ # Change Log | ||
# [1.0.0-alpha.9](https://github.com/adobe/leonardo/compare/@adobe/leonardo-contrast-colors@1.0.0-alpha.8...@adobe/leonardo-contrast-colors@1.0.0-alpha.9) (2021-08-20) | ||
**Note:** Version bump only for package @adobe/leonardo-contrast-colors | ||
# [1.0.0-alpha.8](https://github.com/adobe/leonardo/compare/@adobe/leonardo-contrast-colors@1.0.0-alpha.7...@adobe/leonardo-contrast-colors@1.0.0-alpha.8) (2020-09-08) | ||
@@ -8,0 +16,0 @@ |
@@ -19,3 +19,3 @@ /* | ||
const belzen = exports.bezlen = (x1, y1, x2, y2, x3, y3, x4, y4, z) => { | ||
const bezlen = exports.bezlen = (x1, y1, x2, y2, x3, y3, x4, y4, z) => { | ||
if (z == null) { | ||
@@ -22,0 +22,0 @@ z = 1; |
@@ -35,4 +35,4 @@ /* | ||
start = d3.jch(start); | ||
end = d3.jch(end); | ||
start = d3plus.jch(start); | ||
end = d3plus.jch(end); | ||
@@ -39,0 +39,0 @@ const zero = Math.abs(start.h - end.h); |
@@ -6,3 +6,4 @@ "use strict"; | ||
}); | ||
exports.prepareCurve = exports.catmullRom2bezier = exports.findDotsAtSegment = exports.bezlen = void 0; | ||
exports.bezlen = bezlen; | ||
exports.prepareCurve = exports.catmullRom2bezier = exports.findDotsAtSegment = void 0; | ||
@@ -15,3 +16,3 @@ const base3 = (t, p1, p2, p3, p4) => { | ||
const bezlen = (x1, y1, x2, y2, x3, y3, x4, y4, z) => { | ||
function bezlen(x1, y1, x2, y2, x3, y3, x4, y4, z) { | ||
if (z == null) { | ||
@@ -37,6 +38,4 @@ z = 1; | ||
return z2 * sum; | ||
}; | ||
} | ||
exports.bezlen = bezlen; | ||
const findDotsAtSegment = (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) => { | ||
@@ -43,0 +42,0 @@ const t1 = 1 - t, |
@@ -16,11 +16,11 @@ "use strict"; | ||
var _d = _interopRequireDefault(require("d3")); | ||
var d3 = _interopRequireWildcard(require("d3")); | ||
var d3cam02 = _interopRequireWildcard(require("d3-cam02")); | ||
var _d3Hsluv = _interopRequireDefault(require("d3-hsluv")); | ||
var d3hsluv = _interopRequireWildcard(require("d3-hsluv")); | ||
var d3hsv = _interopRequireWildcard(require("d3-hsv")); | ||
var _curve = require("./curve.js"); | ||
var _curve = require("./curve"); | ||
@@ -31,4 +31,2 @@ function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
/* | ||
@@ -56,3 +54,3 @@ Copyright 2019 Adobe. All rights reserved. | ||
let interpolateJch = (start, end) => { | ||
d3.interpolateJch = (start, end) => { | ||
// constant, linear, and colorInterpolate are taken from d3-interpolate | ||
@@ -69,4 +67,4 @@ // the colorInterpolate function is `nogamma` in the d3-interpolate's color.js | ||
start = _d.default.jch(start); | ||
end = _d.default.jch(end); | ||
start = d3.jch(start); | ||
end = d3.jch(end); | ||
const zero = Math.abs(start.h - end.h); | ||
@@ -84,6 +82,5 @@ const plus = Math.abs(start.h - (end.h + 360)); | ||
const startc = _d.default.hcl(start + '').c; | ||
const startc = d3.hcl(start + '').c; | ||
const endc = d3.hcl(end + '').c; | ||
const endc = _d.default.hcl(end + '').c; | ||
if (!startc) { | ||
@@ -140,4 +137,3 @@ start.h = end.h; | ||
// hue is not important except for JCh | ||
const safeJChHue = _d.default.jch("#ccc").h; | ||
const safeJChHue = d3.jch("#ccc").h; | ||
nans.forEach(j => point[j] = safeJChHue); | ||
@@ -211,7 +207,7 @@ } | ||
return _d.default[space.name](...ch) + ""; | ||
return d3[space.name](...ch) + ""; | ||
}; | ||
} // assign(d3, d3hsluv, d3hsv, d3cam02); | ||
} | ||
assign(d3, d3hsluv, d3hsv, d3cam02); | ||
const colorSpaces = { | ||
@@ -221,3 +217,3 @@ CAM02: { | ||
channels: ['J', 'a', 'b'], | ||
interpolator: _d.default.interpolateJab | ||
interpolator: d3.interpolateJab | ||
}, | ||
@@ -227,3 +223,3 @@ CAM02p: { | ||
channels: ['J', 'C', 'h'], | ||
interpolator: interpolateJch | ||
interpolator: d3.interpolateJch | ||
}, | ||
@@ -233,5 +229,5 @@ LCH: { | ||
channels: ['h', 'c', 'l'], | ||
interpolator: _d.default.interpolateHcl, | ||
white: _d.default.hcl(NaN, 0, 100), | ||
black: _d.default.hcl(NaN, 0, 0) | ||
interpolator: d3.interpolateHcl, | ||
white: d3.hcl(NaN, 0, 100), | ||
black: d3.hcl(NaN, 0, 0) | ||
}, | ||
@@ -241,3 +237,3 @@ LAB: { | ||
channels: ['l', 'a', 'b'], | ||
interpolator: _d.default.interpolateLab | ||
interpolator: d3.interpolateLab | ||
}, | ||
@@ -247,3 +243,3 @@ HSL: { | ||
channels: ['h', 's', 'l'], | ||
interpolator: _d.default.interpolateHsl | ||
interpolator: d3.interpolateHsl | ||
}, | ||
@@ -253,5 +249,5 @@ HSLuv: { | ||
channels: ['l', 'u', 'v'], | ||
interpolator: _d.default.interpolateHsluv, | ||
white: _d3Hsluv.default.hsluv(NaN, NaN, 100), | ||
black: _d3Hsluv.default.hsluv(NaN, NaN, 0) | ||
interpolator: d3.interpolateHsluv, | ||
white: d3.hsluv(NaN, NaN, 100), | ||
black: d3.hsluv(NaN, NaN, 0) | ||
}, | ||
@@ -261,3 +257,3 @@ RGB: { | ||
channels: ['r', 'g', 'b'], | ||
interpolator: _d.default.interpolateRgb | ||
interpolator: d3.interpolateRgb | ||
}, | ||
@@ -267,3 +263,3 @@ HSV: { | ||
channels: ['h', 's', 'v'], | ||
interpolator: _d.default.interpolateHsv | ||
interpolator: d3.interpolateHsv | ||
} | ||
@@ -273,4 +269,3 @@ }; | ||
function cArray(c) { | ||
const color = _d3Hsluv.default.hsluv(c); | ||
const color = d3.hsluv(c); | ||
const L = color.l; | ||
@@ -311,7 +306,6 @@ const U = color.u; | ||
let domains = colorKeys.map(key => swatches - swatches * (_d3Hsluv.default.hsluv(key).v / 100)).sort((a, b) => a - b).concat(swatches); | ||
let domains = colorKeys.map(key => swatches - swatches * (d3.hsluv(key).v / 100)).sort((a, b) => a - b).concat(swatches); | ||
domains.unshift(0); // Test logarithmic domain (for non-contrast-based scales) | ||
let sqrtDomains = _d.default.scalePow().exponent(shift).domain([1, swatches]).range([1, swatches]); | ||
let sqrtDomains = d3.scalePow().exponent(shift).domain([1, swatches]).range([1, swatches]); | ||
sqrtDomains = domains.map(d => { | ||
@@ -354,3 +348,3 @@ if (sqrtDomains(d) < 0) { | ||
const stringColors = ColorsArray; | ||
ColorsArray = ColorsArray.map(d => _d.default[space.name](d)); | ||
ColorsArray = ColorsArray.map(d => d3[space.name](d)); | ||
@@ -366,3 +360,3 @@ if (space.name == 'hcl') { | ||
for (let i = 0; i < stringColors.length; i++) { | ||
const color = _d.default.hcl(stringColors[i]); | ||
const color = d3.hcl(stringColors[i]); | ||
@@ -378,7 +372,6 @@ if (!color.c) { | ||
} else { | ||
scale = _d.default.scaleLinear().range(ColorsArray).domain(domains).interpolate(space.interpolator); | ||
scale = d3.scaleLinear().range(ColorsArray).domain(domains).interpolate(space.interpolator); | ||
} | ||
let Colors = _d.default.range(swatches).map(d => scale(d)); | ||
let Colors = d3.range(swatches).map(d => scale(d)); | ||
let colors = Colors.filter(el => el != null); // Return colors as hex values for interpolators. | ||
@@ -389,3 +382,3 @@ | ||
for (let i = 0; i < colors.length; i++) { | ||
colorsHex.push(_d.default.rgb(colors[i]).formatHex()); | ||
colorsHex.push(d3.rgb(colors[i]).formatHex()); | ||
} | ||
@@ -464,11 +457,9 @@ | ||
}); | ||
let baseV = _d3Hsluv.default.hsluv(base).v / 100; | ||
let Contrasts = _d.default.range(swatches).map(d => { | ||
let rgbArray = [_d.default.rgb(scaleData.scale(d)).r, _d.default.rgb(scaleData.scale(d)).g, _d.default.rgb(scaleData.scale(d)).b]; | ||
let baseRgbArray = [_d.default.rgb(base).r, _d.default.rgb(base).g, _d.default.rgb(base).b]; | ||
let baseV = d3.hsluv(base).v / 100; | ||
let Contrasts = d3.range(swatches).map(d => { | ||
let rgbArray = [d3.rgb(scaleData.scale(d)).r, d3.rgb(scaleData.scale(d)).g, d3.rgb(scaleData.scale(d)).b]; | ||
let baseRgbArray = [d3.rgb(base).r, d3.rgb(base).g, d3.rgb(base).b]; | ||
let ca = contrast(rgbArray, baseRgbArray, baseV).toFixed(2); | ||
return Number(ca); | ||
}); | ||
let contrasts = Contrasts.filter(el => el != null); | ||
@@ -480,3 +471,3 @@ let newColors = []; | ||
let r = binarySearch(contrasts, ratios[i], baseV); | ||
newColors.push(_d.default.rgb(scaleData.colors[r]).hex()); | ||
newColors.push(d3.rgb(scaleData.colors[r]).hex()); | ||
} | ||
@@ -617,3 +608,3 @@ | ||
let baseMode = colorScales[baseIndex].colorspace; | ||
let smooth = colorScales[baseIndex].smooth; // define params to pass as bscale | ||
let baseSmooth = colorScales[baseIndex].smooth; // define params to pass as bscale | ||
@@ -623,3 +614,3 @@ let bscale = generateBaseScale({ | ||
colorspace: baseMode, | ||
smooth: smooth | ||
smooth: baseSmooth | ||
}); // base parameter to create base scale (0-100) | ||
@@ -626,0 +617,0 @@ |
@@ -17,3 +17,3 @@ /* | ||
declare namespace ContrastColors { | ||
type InterpolationColorspace = 'CAM02' | 'LCH' | 'LAB' | 'HSL' | 'HSLuv' | 'RGB' | 'HSV'; | ||
type InterpolationColorspace = 'CAM02' | 'CAM02p' | 'LCH' | 'LAB' | 'HSL' | 'HSLuv' | 'RGB' | 'HSV'; | ||
type Colorspace = 'CAM02' | 'CAM02p' | 'LCH' | 'LAB' | 'HSL' | 'HSLuv' | 'RGB' | 'HSV' | 'HEX'; | ||
@@ -23,3 +23,3 @@ | ||
type AdaptiveTheme = (brightness: number, constrast: number) => AdaptiveTheme | ({ | ||
type AdaptiveTheme = (brightness: number, constrast?: number) => AdaptiveTheme | ({ | ||
background: string | ||
@@ -26,0 +26,0 @@ } | { |
886
index.js
@@ -16,4 +16,477 @@ /* | ||
const { catmullRom2bezier, prepareCurve } = require('./curve.js'); | ||
const { color } = require('./d3.js'); | ||
// const { color } = require('./d3.js'); | ||
class Theme { | ||
constructor({colors, backgroundColor, lightness, contrast = 1, output = 'HEX'}) { | ||
this._output = output; | ||
this._colors = colors; | ||
this._lightness = lightness; | ||
this._setBackgroundColor(backgroundColor); | ||
this._setBackgroundColorValue(); | ||
this._contrast = contrast; | ||
if (!this._colors) { | ||
throw new Error(`No colors are defined`); | ||
} | ||
if (!this._backgroundColor) { | ||
throw new Error(`Background color is undefined`); | ||
} | ||
colors.forEach(color => { | ||
if(!color.ratios) throw new Error(`Color ${color.name}'s ratios are undefined`); | ||
}); | ||
if (!colorSpaces[this._output]) { | ||
throw new Error(`Output “${colorspace}” not supported`); | ||
} | ||
this._modifiedColors = this._colors; | ||
// console.log(`${this._colors} \n ----------------- \n ${this._modifiedColors}`) | ||
// this._setContrasts(this._contrast); | ||
this._findContrastColors(); | ||
this._findContrastColorValues(); | ||
} | ||
set contrast(contrast) { | ||
this._contrast = contrast; | ||
// this._setContrasts(contrast); | ||
this._findContrastColors(); | ||
} | ||
get contrast() { | ||
return this._contrast; | ||
} | ||
set lightness(lightness) { | ||
this._lightness = lightness; | ||
this._setBackgroundColor(this._backgroundColor); | ||
this._findContrastColors(); | ||
} | ||
get lightness() { | ||
return this._lightness; | ||
} | ||
set backgroundColor(backgroundColor) { | ||
this._setBackgroundColor(backgroundColor); | ||
this._findContrastColors(); | ||
} | ||
get backgroundColorValue() { | ||
return this._backgroundColorValue; | ||
} | ||
get backgroundColor() { | ||
return this._backgroundColor; | ||
} | ||
// Add a getter and setter for colors | ||
set colors(colors) { | ||
this._colors = colors; | ||
this._findContrastColors(); | ||
} | ||
get colors() { | ||
return this._colors; | ||
} | ||
set output(output) { | ||
this._output = output; | ||
this._colors.forEach(element => { | ||
element.output = this._output; | ||
}); | ||
this._backgroundColor.output = this._output; | ||
this._findContrastColors(); | ||
} | ||
get output() { | ||
return this._output; | ||
} | ||
get contrastColors() { | ||
return this._contrastColors; | ||
} | ||
get contrastColorValues() { | ||
return this._contrastColorValues; | ||
} | ||
_setBackgroundColor(backgroundColor) { | ||
if(typeof backgroundColor === 'string') { | ||
// If it's a string, convert to Color object and assign lightness. | ||
const newBackgroundColor = new BackgroundColor({name: 'background', colorKeys: [backgroundColor], output: 'RGB'}); | ||
const calcLightness = Number((d3.hsluv(backgroundColor).v).toFixed()); | ||
return this._backgroundColor = newBackgroundColor, this._lightness = calcLightness, this._backgroundColorValue = newBackgroundColor[this._lightness]; | ||
// console.log(`String background color of ${backgroundColor} converted to ${newBackgroundColor}`) | ||
} else { | ||
// console.log(`NOT a string for background, instead it is ${JSON.stringify(backgroundColor)}`) | ||
backgroundColor.output = 'RGB'; | ||
const calcBackgroundColorValue = backgroundColor.backgroundColorScale[this._lightness]; | ||
// console.log(`Object background \nLightness: ${this._lightness} \nBackground scale: ${backgroundColor.backgroundColorScale}\nCalculated background value of ${calcBackgroundColorValue}`) | ||
return this._backgroundColor = backgroundColor, this._backgroundColorValue = calcBackgroundColorValue; | ||
} | ||
} | ||
_setBackgroundColorValue() { | ||
return this._backgroundColorValue = this._backgroundColor.backgroundColorScale[this._lightness]; | ||
} | ||
_findContrastColors() { | ||
const bgRgbArray = [d3.rgb(this._backgroundColorValue).r, d3.rgb(this._backgroundColorValue).g, d3.rgb(this._backgroundColorValue).b]; | ||
const baseV = this._lightness / 100; | ||
let baseObj = { | ||
background: convertColorValue(this._backgroundColorValue, this._output), | ||
}; | ||
let returnColors = []; // Array to be populated with JSON objects for each color, including names & contrast values | ||
let returnColorValues = []; // Array to be populated with flat list of all color values | ||
returnColors.push(baseObj); | ||
this._modifiedColors.map(color => { | ||
if (color.ratios !== undefined) { | ||
let swatchNames; | ||
let newArr = []; | ||
let colorObj = { | ||
name: color.name, | ||
values: newArr | ||
}; | ||
// This needs to be looped for each value in the color.colorScale array | ||
// Keeping the number of contrasts calculated equal to the number of colors | ||
// available in each color's colorScale array | ||
let contrasts = d3.range(color.colorScale.length).map((d) => { | ||
let rgbArray = [d3.rgb(color.colorScale[d]).r, d3.rgb(color.colorScale[d]).g, d3.rgb(color.colorScale[d]).b]; | ||
let ca = contrast(rgbArray, bgRgbArray, baseV).toFixed(2); | ||
return Number(ca); | ||
}); | ||
contrasts = contrasts.filter(el => el != null); | ||
let contrastColors = []; | ||
let ratioLength; | ||
let ratioValues; | ||
if(Array.isArray(color.ratios)) { | ||
ratioLength = color.ratios.length; | ||
ratioValues = color.ratios; | ||
} else if (!Array.isArray(color.ratios)){ | ||
ratioLength = Object.keys(color.ratios).length; | ||
swatchNames = Object.keys(color.ratios); | ||
ratioValues = Object.values(color.ratios); | ||
} | ||
// modify target ratio based on contrast multiplier | ||
let newRatioValues = ratioValues.map(ratio => multiplyRatios(ratio, this._contrast) ); | ||
if(this._contrast !==1) ratioValues = newRatioValues; | ||
// Return color matching target ratio, or closest number | ||
for (let i=0; i < ratioLength; i++){ | ||
// Find the index of each target ratio in the array of all possible contrasts | ||
let r = getMatchingRatioIndex(contrasts, ratioValues[i]); | ||
let match = color.colorScale[r]; | ||
// Use the index from matching contrasts (r) to index the corresponding | ||
// color value from the color scale array. | ||
// use convertColorValue function to convert each color to the specified | ||
// output format and push to the new array 'contrastColors' | ||
contrastColors.push(convertColorValue(match, this._output)); | ||
} | ||
for (let i=0; i < contrastColors.length; i++) { | ||
let n; | ||
if(!swatchNames) { | ||
let rVal = ratioName(color.ratios)[i]; | ||
n = color.name.concat(rVal); | ||
} | ||
else { | ||
n = swatchNames[i]; | ||
} | ||
let obj = { | ||
name: n, | ||
contrast: ratioValues[i], | ||
value: contrastColors[i] | ||
}; | ||
newArr.push(obj); | ||
// Push the same value to the returnColorValues array | ||
returnColorValues.push(contrastColors[i]); | ||
} | ||
returnColors.push(colorObj); | ||
} | ||
}); | ||
this._contrastColorValues = returnColorValues; | ||
this._contrastColors = returnColors; | ||
return this._contrastColors; | ||
} | ||
_findContrastColorValues() { | ||
return this._contrastColorValues; | ||
} | ||
} | ||
class Color { | ||
constructor({name, colorKeys, colorspace = 'RGB', ratios, smooth = false, output = 'HEX'}) { | ||
this._name = name; | ||
this._colorKeys = colorKeys; | ||
this._colorspace = colorspace; | ||
this._ratios = ratios; | ||
this._smooth = smooth; | ||
this._output = output; | ||
if (!this._name) { | ||
throw new Error('Color missing name'); | ||
} | ||
if (!this._colorKeys) { | ||
throw new Error(`Color Keys are undefined`); | ||
} | ||
if (!colorSpaces[this._colorspace]) { | ||
throw new Error(`Colorspace “${colorspace}” not supported`); | ||
} | ||
if (!colorSpaces[this._output]) { | ||
throw new Error(`Output “${colorspace}” not supported`); | ||
} | ||
// validate color keys | ||
for (let i=0; i<this._colorKeys.length; i++) { | ||
if (this._colorKeys[i].length < 6) { | ||
throw new Error('Color Key must be greater than 6 and include hash # if hex.'); | ||
} | ||
else if (this._colorKeys[i].length == 6 && this._colorKeys[i].charAt(0) != 0) { | ||
throw new Error('Color Key missing hash #'); | ||
} | ||
} | ||
// Run function to generate this array of colors: | ||
this._generateColorScale(); | ||
} | ||
// Setting and getting properties of the Color class | ||
set colorKeys(colorKeys) { | ||
this._colorKeys = colorKeys; | ||
this._generateColorScale() | ||
} | ||
get colorKeys() { | ||
return this._colorKeys; | ||
} | ||
set colorspace(colorspace) { | ||
this._colorspace = colorspace; | ||
this._generateColorScale() | ||
} | ||
get colorspace() { | ||
return this._colorspace; | ||
} | ||
set ratios(ratios) { | ||
this._ratios = ratios; | ||
} | ||
get ratios() { | ||
return this._ratios; | ||
} | ||
set name(name) { | ||
this._name = name; | ||
} | ||
get name () { | ||
return this._name; | ||
} | ||
set smooth(smooth) { | ||
this._smooth = smooth; | ||
this._generateColorScale() | ||
} | ||
get smooth() { | ||
return this._smooth; | ||
} | ||
set output(output) { | ||
this._output = output; | ||
this._generateColorScale() | ||
} | ||
get output() { | ||
return this._output; | ||
} | ||
get colorScale() { | ||
return this._colorScale; | ||
} | ||
_generateColorScale() { | ||
// This would create 3000 color values based on all parameters | ||
// and return an array of colors: | ||
const colorScale = createScale({swatches: 3000, colorKeys: this._colorKeys, colorspace: this._colorspace, shift: 1, smooth: this._smooth}); | ||
colorScale.map(color => { | ||
return convertColorValue(color, this._output); | ||
}); | ||
// Remove duplicate color values | ||
this._colorScale = uniq(colorScale); | ||
return this._colorScale; | ||
} | ||
} | ||
class BackgroundColor extends Color { | ||
constructor(options) { | ||
super(options) | ||
} | ||
get backgroundColorScale() { | ||
return this._backgroundColorScale; | ||
} | ||
_generateColorScale() { | ||
// This would create a 100 color value array based on all parameters, | ||
// which can be used for sliding lightness as a background color | ||
// Call original generateColorScale method in the context of our background color | ||
// Then we can run the code for Color, but we've added in more below. | ||
Color.prototype._generateColorScale.call(this); | ||
// create massive scale | ||
let backgroundColorScale = createScale({swatches: 1000, colorKeys: this._colorKeys, colorspace: this._colorspace, shift: 1, smooth: this._smooth}); | ||
// Inject original keycolors to ensure they are present in the background options | ||
backgroundColorScale.push(this.colorKeys); | ||
let colorObj = backgroundColorScale | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return { value: Math.round(cArray(c)[2]), index: i } }); | ||
let bgColorArrayFiltered = removeDuplicates(colorObj, "value") | ||
.map(data => backgroundColorScale[data.index]); | ||
// Manually cap the background array at 100 colors, then add white back to the end | ||
// since it sometimes gets removed. | ||
bgColorArrayFiltered.length = 100; | ||
bgColorArrayFiltered.push('#ffffff'); | ||
this._backgroundColorScale = bgColorArrayFiltered.map(color => { | ||
return convertColorValue(color, this._output); | ||
}); | ||
return this._backgroundColorScale; | ||
} | ||
} | ||
/** | ||
* Utility functions | ||
*/ | ||
function multiplyRatios(ratio, multiplier) { | ||
let r; | ||
// Normalize contrast ratios before multiplying by this._contrast | ||
// by making 1 = 0. This ensures consistent application of increase/decrease | ||
// in contrast ratios. Then add 1 back to number for contextual ratio value. | ||
if(ratio > 1) { | ||
r = ((ratio-1) * multiplier) + 1; | ||
} | ||
else if(ratio < -1) { | ||
r = ((ratio+1) * multiplier) - 1; | ||
} | ||
else { | ||
r = 1; | ||
} | ||
return Number(r.toFixed(2)); | ||
} | ||
function createScale({ | ||
swatches, | ||
colorKeys, | ||
colorspace = 'LAB', | ||
shift = 1, | ||
fullScale = true, | ||
smooth = false | ||
} = {}) { | ||
const space = colorSpaces[colorspace]; | ||
if (!space) { | ||
throw new Error(`Colorspace “${colorspace}” not supported`); | ||
} | ||
if (!colorKeys) { | ||
throw new Error(`Colorkeys missing: returned “${colorKeys}”`); | ||
} | ||
let domains = colorKeys | ||
.map(key => swatches - swatches * (d3.hsluv(key).v / 100)) | ||
.sort((a, b) => a - b) | ||
.concat(swatches); | ||
domains.unshift(0); | ||
// Test logarithmic domain (for non-contrast-based scales) | ||
let sqrtDomains = d3.scalePow() | ||
.exponent(shift) | ||
.domain([1, swatches]) | ||
.range([1, swatches]); | ||
sqrtDomains = domains.map((d) => { | ||
if (sqrtDomains(d) < 0) { | ||
return 0; | ||
} | ||
return sqrtDomains(d); | ||
}); | ||
// Transform square root in order to smooth gradient | ||
domains = sqrtDomains; | ||
let sortedColor = colorKeys | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return { colorKeys: cArray(c), index: i } }) | ||
// Sort by lightness | ||
.sort((c1, c2) => c2.colorKeys[2] - c1.colorKeys[2]) | ||
// Retrieve original RGB color | ||
.map(data => colorKeys[data.index]); | ||
let inverseSortedColor = colorKeys | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return {colorKeys: cArray(c), index: i} }) | ||
// Sort by lightness | ||
.sort((c1, c2) => c1.colorKeys[2] - c2.colorKeys[2]) | ||
// Retrieve original RGB color | ||
.map(data => colorKeys[data.index]); | ||
let ColorsArray = []; | ||
let scale; | ||
if (fullScale) { | ||
ColorsArray = [space.white || '#ffffff', ...sortedColor, space.black || '#000000']; | ||
} else { | ||
ColorsArray = sortedColor; | ||
} | ||
const stringColors = ColorsArray; | ||
ColorsArray = ColorsArray.map(d => d3[space.name](d)); | ||
if (space.name == 'hcl') { | ||
// special case for HCL if C is NaN we should treat it as 0 | ||
ColorsArray.forEach(c => c.c = isNaN(c.c) ? 0 : c.c); | ||
} | ||
if (space.name == 'jch') { | ||
// JCh has some “random” hue for grey colors. | ||
// Replacing it to NaN, so we can apply the same method of dealing with them. | ||
for (let i = 0; i < stringColors.length; i++) { | ||
const color = d3.hcl(stringColors[i]); | ||
if (!color.c) { | ||
ColorsArray[i].h = NaN; | ||
} | ||
} | ||
} | ||
if (smooth) { | ||
scale = smoothScale(ColorsArray, domains, space); | ||
} else { | ||
scale = d3.scaleLinear() | ||
.range(ColorsArray) | ||
.domain(domains) | ||
.interpolate(space.interpolator); | ||
} | ||
let Colors = d3.range(swatches).map(d => scale(d)); | ||
let colors = Colors.filter(el => el != null); | ||
return colors; | ||
} | ||
function smoothScale(ColorsArray, domains, space) { | ||
@@ -196,189 +669,6 @@ const points = space.channels.map(() => []); | ||
function createScale({ | ||
swatches, | ||
colorKeys, | ||
colorspace = 'LAB', | ||
shift = 1, | ||
fullScale = true, | ||
smooth = false | ||
} = {}) { | ||
const space = colorSpaces[colorspace]; | ||
if (!space) { | ||
throw new Error(`Colorspace “${colorspace}” not supported`); | ||
} | ||
let domains = colorKeys | ||
.map(key => swatches - swatches * (d3.hsluv(key).v / 100)) | ||
.sort((a, b) => a - b) | ||
.concat(swatches); | ||
domains.unshift(0); | ||
// Test logarithmic domain (for non-contrast-based scales) | ||
let sqrtDomains = d3.scalePow() | ||
.exponent(shift) | ||
.domain([1, swatches]) | ||
.range([1, swatches]); | ||
sqrtDomains = domains.map((d) => { | ||
if (sqrtDomains(d) < 0) { | ||
return 0; | ||
} | ||
return sqrtDomains(d); | ||
}); | ||
// Transform square root in order to smooth gradient | ||
domains = sqrtDomains; | ||
let sortedColor = colorKeys | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return { colorKeys: cArray(c), index: i } }) | ||
// Sort by lightness | ||
.sort((c1, c2) => c2.colorKeys[2] - c1.colorKeys[2]) | ||
// Retrieve original RGB color | ||
.map(data => colorKeys[data.index]); | ||
let inverseSortedColor = colorKeys | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return {colorKeys: cArray(c), index: i} }) | ||
// Sort by lightness | ||
.sort((c1, c2) => c1.colorKeys[2] - c2.colorKeys[2]) | ||
// Retrieve original RGB color | ||
.map(data => colorKeys[data.index]); | ||
let ColorsArray = []; | ||
let scale; | ||
if (fullScale) { | ||
ColorsArray = [space.white || '#fff', ...sortedColor, space.black || '#000']; | ||
} else { | ||
ColorsArray = sortedColor; | ||
} | ||
const stringColors = ColorsArray; | ||
ColorsArray = ColorsArray.map(d => d3[space.name](d)); | ||
if (space.name == 'hcl') { | ||
// special case for HCL if C is NaN we should treat it as 0 | ||
ColorsArray.forEach(c => c.c = isNaN(c.c) ? 0 : c.c); | ||
} | ||
if (space.name == 'jch') { | ||
// JCh has some “random” hue for grey colors. | ||
// Replacing it to NaN, so we can apply the same method of dealing with them. | ||
for (let i = 0; i < stringColors.length; i++) { | ||
const color = d3.hcl(stringColors[i]); | ||
if (!color.c) { | ||
ColorsArray[i].h = NaN; | ||
} | ||
} | ||
} | ||
if (smooth) { | ||
scale = smoothScale(ColorsArray, domains, space); | ||
} else { | ||
scale = d3.scaleLinear() | ||
.range(ColorsArray) | ||
.domain(domains) | ||
.interpolate(space.interpolator); | ||
} | ||
let Colors = d3.range(swatches).map(d => scale(d)); | ||
let colors = Colors.filter(el => el != null); | ||
// Return colors as hex values for interpolators. | ||
let colorsHex = []; | ||
for (let i = 0; i < colors.length; i++) { | ||
colorsHex.push(d3.rgb(colors[i]).formatHex()); | ||
} | ||
return { | ||
colorKeys: colorKeys, | ||
colorspace: colorspace, | ||
shift: shift, | ||
colors: colors, | ||
scale: scale, | ||
colorsHex: colorsHex | ||
}; | ||
function uniq(a) { | ||
return Array.from(new Set(a)); | ||
} | ||
function generateBaseScale({ | ||
colorKeys, | ||
colorspace = 'LAB', | ||
smooth | ||
} = {}) { | ||
// create massive scale | ||
let swatches = 1000; | ||
let scale = createScale({swatches: swatches, colorKeys: colorKeys, colorspace: colorspace, shift: 1, smooth: smooth}); | ||
let newColors = scale.colorsHex; | ||
let colorObj = newColors | ||
// Convert to HSLuv and keep track of original indices | ||
.map((c, i) => { return { value: Math.round(cArray(c)[2]), index: i } }); | ||
let filteredArr = removeDuplicates(colorObj, "value") | ||
.map(data => newColors[data.index]); | ||
return filteredArr; | ||
} | ||
function generateContrastColors({ | ||
colorKeys, | ||
base, | ||
ratios, | ||
colorspace = 'LAB', | ||
smooth = false, | ||
output = 'HEX' | ||
} = {}) { | ||
if (!base) { | ||
throw new Error(`Base is undefined`); | ||
} | ||
if (!colorKeys) { | ||
throw new Error(`Color Keys are undefined`); | ||
} | ||
for (let i=0; i<colorKeys.length; i++) { | ||
if (colorKeys[i].length < 6) { | ||
throw new Error('Color Key must be greater than 6 and include hash # if hex.'); | ||
} | ||
else if (colorKeys[i].length == 6 && colorKeys[i].charAt(0) != 0) { | ||
throw new Error('Color Key missing hash #'); | ||
} | ||
} | ||
if (!ratios) { | ||
throw new Error(`Ratios are undefined`); | ||
} | ||
const outputFormat = colorSpaces[output]; | ||
if (!outputFormat) { | ||
throw new Error(`Colorspace “${output}” not supported`); | ||
} | ||
let swatches = 3000; | ||
let scaleData = createScale({swatches: swatches, colorKeys: colorKeys, colorspace: colorspace, shift: 1, smooth: smooth}); | ||
let baseV = (d3.hsluv(base).v) / 100; | ||
let Contrasts = d3.range(swatches).map((d) => { | ||
let rgbArray = [d3.rgb(scaleData.scale(d)).r, d3.rgb(scaleData.scale(d)).g, d3.rgb(scaleData.scale(d)).b]; | ||
let baseRgbArray = [d3.rgb(base).r, d3.rgb(base).g, d3.rgb(base).b]; | ||
let ca = contrast(rgbArray, baseRgbArray, baseV).toFixed(2); | ||
return Number(ca); | ||
}); | ||
let contrasts = Contrasts.filter(el => el != null); | ||
let newColors = []; | ||
ratios = ratios.map(Number); | ||
// Return color matching target ratio, or closest number | ||
for (let i=0; i < ratios.length; i++){ | ||
let r = binarySearch(contrasts, ratios[i], baseV); | ||
// 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 | ||
@@ -394,3 +684,10 @@ function filterNaN(x) { | ||
// Helper function for rounding color values to whole numbers | ||
function fixColorValue(color, format, object = false) { | ||
function convertColorValue(color, format, object = false) { | ||
if(!color) { | ||
throw new Error(`Cannot convert color value of ${color}`) | ||
} | ||
if(!format) { | ||
throw new Error(`Cannot convert to colorspace ${format}`) | ||
} | ||
let colorObj = colorSpaces[format].function(color); | ||
@@ -494,23 +791,42 @@ let propArray = colorSpaces[format].channels; | ||
function contrast(color, base, baseV) { | ||
if(baseV == undefined) { // If base is an array and baseV undefined | ||
let colorString = String(`rgb(${base[0]}, ${base[1]}, ${base[2]})`); | ||
baseLightness = Number((d3.hsluv(colorString).v)); | ||
if(baseLightness > 0) { | ||
baseV = Number((baseLightness / 100).toFixed(2)); | ||
} else if (baseLightness === 0) { | ||
baseV = 0; | ||
} | ||
} | ||
let colorLum = luminance(color[0], color[1], color[2]); | ||
let baseLum = luminance(base[0], base[1], base[2]); | ||
let cr1 = (colorLum + 0.05) / (baseLum + 0.05); | ||
let cr2 = (baseLum + 0.05) / (colorLum + 0.05); | ||
if (baseV < 0.5) { | ||
let cr1 = (colorLum + 0.05) / (baseLum + 0.05); // will return value >=1 if color is darker than background | ||
let cr2 = (baseLum + 0.05) / (colorLum + 0.05); // will return value >=1 if color is lighter than background | ||
if (baseV <= 0.51) { // Dark themes | ||
// If color is darker than background, return cr1 which will be whole number | ||
if (cr1 >= 1) { | ||
return cr1; | ||
} | ||
// If color is lighter than background, return cr2 as negative whole number | ||
else { | ||
return cr2 * -1; | ||
} // Return as whole negative number | ||
} | ||
} | ||
else { | ||
else { // Light themes | ||
// If color is lighter than background, return cr2 which will be whole number | ||
if (cr1 < 1) { | ||
return cr2; | ||
} | ||
// If color is darker than background, return cr1 as negative whole number | ||
else if (cr1 === 1) { | ||
return cr1; | ||
} | ||
else { | ||
return cr1 * -1; | ||
} // Return as whole negative number | ||
} | ||
} | ||
@@ -559,136 +875,8 @@ } | ||
function generateAdaptiveTheme({ | ||
colorScales, | ||
baseScale, | ||
brightness, | ||
contrast = 1, | ||
output = 'HEX' | ||
}) { | ||
if (!baseScale) { | ||
throw new Error('baseScale is undefined'); | ||
} | ||
let found = false; | ||
for(let i = 0; i < colorScales.length; i++) { | ||
if (colorScales[i].name !== baseScale) { | ||
found = true; | ||
} | ||
} | ||
if (found = false) { | ||
throw new Error('baseScale must match the name of a colorScales object'); | ||
} | ||
if (!colorScales) { | ||
throw new Error('colorScales are undefined'); | ||
} | ||
if (!Array.isArray(colorScales)) { | ||
throw new Error('colorScales must be an array of objects'); | ||
} | ||
for (let i=0; i < colorScales.length; i ++) { | ||
// if (colorScales[i].swatchNames) { // if the scale has custom swatch names | ||
// let ratioLength = colorScales[i].ratios.length; | ||
// let swatchNamesLength = colorScales[i].swatchNames.length; | ||
// if (ratioLength !== swatchNamesLength) { | ||
// throw new Error('`${colorScales[i].name}`ratios and swatchNames must be equal length') | ||
// } | ||
// } | ||
} | ||
if (brightness === undefined) { | ||
return function(brightness, contrast) { | ||
return generateAdaptiveTheme({baseScale: baseScale, colorScales: colorScales, brightness: brightness, contrast: contrast, output: output}); | ||
} | ||
} | ||
else { | ||
// Find color object matching base scale | ||
let baseIndex = colorScales.findIndex( x => x.name === baseScale ); | ||
let baseKeys = colorScales[baseIndex].colorKeys; | ||
let baseMode = colorScales[baseIndex].colorspace; | ||
let smooth = colorScales[baseIndex].smooth; | ||
// define params to pass as bscale | ||
let bscale = generateBaseScale({colorKeys: baseKeys, colorspace: baseMode, smooth: smooth}); // base parameter to create base scale (0-100) | ||
let bval = bscale[brightness]; | ||
let baseObj = { | ||
background: bval | ||
}; | ||
let arr = []; | ||
arr.push(baseObj); | ||
for (let i = 0; i < colorScales.length; i++) { | ||
if (!colorScales[i].name) { | ||
throw new Error('Color missing name'); | ||
} | ||
let name = colorScales[i].name; | ||
let ratioInput = colorScales[i].ratios; | ||
let ratios; | ||
let swatchNames; | ||
// assign ratios array whether input is array or object | ||
if(Array.isArray(ratioInput)) { | ||
ratios = ratioInput; | ||
} else { | ||
ratios = Object.values(ratioInput); | ||
swatchNames = Object.keys(ratioInput); | ||
} | ||
let smooth = colorScales[i].smooth; | ||
let newArr = []; | ||
let colorObj = { | ||
name: name, | ||
values: newArr | ||
}; | ||
ratios = ratios.map(function(d) { | ||
let r; | ||
if(d > 1) { | ||
r = ((d-1) * contrast) + 1; | ||
} | ||
else if(d < -1) { | ||
r = ((d+1) * contrast) - 1; | ||
} | ||
else { | ||
r = 1; | ||
} | ||
return Number(r.toFixed(2)); | ||
}); | ||
let outputColors = generateContrastColors({ | ||
colorKeys: colorScales[i].colorKeys, | ||
colorspace: colorScales[i].colorspace, | ||
ratios: ratios, | ||
base: bval, | ||
smooth: smooth, | ||
output: output | ||
}); | ||
for (let i=0; i < outputColors.length; i++) { | ||
let n; | ||
if(!swatchNames) { | ||
let rVal = ratioName(ratios)[i]; | ||
n = name.concat(rVal); | ||
} | ||
else { | ||
n = swatchNames[i]; | ||
} | ||
let obj = { | ||
name: n, | ||
contrast: ratios[i], | ||
value: outputColors[i] | ||
}; | ||
newArr.push(obj) | ||
} | ||
arr.push(colorObj); | ||
} | ||
return arr; | ||
} | ||
} | ||
// Binary search to find index of contrast ratio that is input | ||
// Modified from https://medium.com/hackernoon/programming-with-js-binary-search-aaf86cef9cb3 | ||
function binarySearch(list, value, baseLum) { | ||
function getMatchingRatioIndex(list, value) { | ||
// If a value of -1 is passed, it should be positive since 1 is the zero-point | ||
if(value === -1) value = 1; | ||
// initial values for start, middle and end | ||
@@ -698,11 +886,9 @@ let start = 0 | ||
let middle = Math.floor((start + stop) / 2) | ||
let descending = list[0] > list[list.length - 1]; | ||
let positiveValue = Math.sign(value) === 1; | ||
let minContrast = Math.min(...list); | ||
let maxContrast = Math.max(...list); | ||
// While the middle is not what we're looking for and the list does not have a single item | ||
while (list[middle] !== value && start < stop) { | ||
// Value greater than since array is ordered descending | ||
if (baseLum > 0.5) { // if base is light, ratios ordered ascending | ||
if (value < list[middle]) { | ||
if (descending) { // descending list | ||
if (value > list[middle]) { | ||
stop = middle - 1 | ||
@@ -713,9 +899,9 @@ } | ||
} | ||
} | ||
else { // order descending | ||
} | ||
else { // ascending list | ||
if (value > list[middle]) { | ||
stop = middle - 1 | ||
start = middle + 1 | ||
} | ||
else { | ||
start = middle + 1 | ||
stop = middle - 1 | ||
} | ||
@@ -727,7 +913,21 @@ } | ||
// If no match, find closest item greater than value | ||
let closest = list.reduce((prev, curr) => curr > value ? curr : prev); | ||
// Create mini array focusing around the middle value | ||
// Shift the middle value if it's on either end of the list array | ||
// and create a new start/stop for the new array based on the new middle | ||
let newMiddle = middle === 0 ? middle + 2 : ((middle === list.length - 1) ? middle - 2 : middle); | ||
let newArray = list.slice(newMiddle - 1, newMiddle + 2); | ||
// if the current middle item is what we're looking for return it's index, else closest | ||
return (list[middle] == !value) ? closest : middle // how it was originally expressed | ||
// Then, find the next larger positive number or next smaller negative number from that array | ||
// let nextClosestValue = ((value >= newMax && positiveValue) || (value <= newMax && positiveValue === false)) ? newMax : (((value <= newMin && positiveValue) || (value >= newMin && positiveValue === false)) ? newMin : (value > 0) ? newArray.find(element => element > value) : newArray.find(element => element < value)); | ||
let nextLargestValue = (list[newMiddle] >= value) ? list[newMiddle] : newArray.find(element => element > value); | ||
let nextSmallestValue = (list[newMiddle] <= value) ? list[newMiddle] : newArray.find(element => element < value); | ||
let nextClosestValue = (positiveValue === true) ? nextLargestValue : nextSmallestValue; | ||
let result = list[middle] !== value && nextClosestValue !== undefined ? (((descending && value <= 1) || (!descending && value > 1)) ? list.lastIndexOf(nextClosestValue) : list.indexOf(nextClosestValue)) : middle; | ||
// To be extra safe, cap the possible result index | ||
// to be no less than 0 and no greater than the list's length: | ||
if (result < 0) return 0; | ||
if (result > list.length - 1) return list.length - 1; | ||
else return result; | ||
} | ||
@@ -739,9 +939,9 @@ | ||
contrast, | ||
binarySearch, | ||
generateBaseScale, | ||
generateContrastColors, | ||
getMatchingRatioIndex, | ||
minPositive, | ||
ratioName, | ||
generateAdaptiveTheme, | ||
fixColorValue | ||
convertColorValue, | ||
Color, | ||
BackgroundColor, | ||
Theme | ||
}; |
{ | ||
"name": "@adobe/leonardo-contrast-colors", | ||
"version": "1.0.0-alpha.8", | ||
"version": "1.0.0-alpha.9", | ||
"description": "Generate colors based on a desired contrast ratio", | ||
@@ -21,12 +21,15 @@ "repository": "git@github.com:adobe/leonardo.git", | ||
"dependencies": { | ||
"d3": "^5.14.2", | ||
"d3-3d": "0.0.9", | ||
"d3-cam02": "^0.1.5", | ||
"d3-color": "^3.0.1", | ||
"d3-hsluv": "^0.1.2", | ||
"d3-hsv": "^0.1.0" | ||
"d3-hsv": "^0.1.0", | ||
"d3-interpolate": "^3.0.1" | ||
}, | ||
"devDependencies": { | ||
"jest": "^26.6.3" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"gitHead": "636c6d29b83bd392db0cb63c4d908c5dc802b940" | ||
"gitHead": "1fb08b86af0c7aa1a90b7cc66e0278c13c6725dd" | ||
} |
440
README.md
@@ -21,3 +21,3 @@ # `@adobe/leonardo-contrast-colors` | ||
```js | ||
const { generateAdaptiveTheme } = require('@adobe/leonardo-contrast-colors'); | ||
const { Theme, Color, BackgroundColor } = require('@adobe/leonardo-contrast-colors'); | ||
``` | ||
@@ -28,96 +28,59 @@ | ||
```js | ||
import { generateAdaptiveTheme } from '@adobe/leonardo-contrast-colors'; | ||
import { Theme, Color, BackgroundColor } from '@adobe/leonardo-contrast-colors'; | ||
``` | ||
### Pass your colors and desired ratios (see additional options below): | ||
### Create and pass colors and a background color to a new Theme (see additional options below): | ||
```js | ||
// returns theme colors as JSON | ||
let myTheme = generateAdaptiveTheme({ | ||
colorScales: [ | ||
{ | ||
let gray = new BackgroundColor({ | ||
name: 'gray', | ||
colorKeys: ['#cacaca'], | ||
ratios: { | ||
'GRAY_LOW_CONTRAST': 2, | ||
'GRAY_LARGE_TEXT': 3, | ||
'GRAY_TEXT': 4.5, | ||
'GRAY_HIGH_CONTRAST': 8 | ||
} | ||
}, | ||
{ | ||
ratios: [2, 3, 4.5, 8] | ||
}); | ||
let blue = new Color({ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
ratios: { | ||
'BLUE_LARGE_TEXT': 3, | ||
'BLUE_TEXT': 4.5 | ||
} | ||
}, | ||
{ | ||
ratios: [3, 4.5] | ||
}); | ||
let red = new Color({ | ||
name: 'red', | ||
colorKeys: ['#FF9A81', '#FF0000'], | ||
ratios: { | ||
'RED_LARGE_TEXT': 3, | ||
'RED_TEXT': 4.5 | ||
} | ||
} | ||
], | ||
baseScale: 'gray', | ||
brightness: 97 | ||
}); | ||
``` | ||
ratios: [3, 4.5] | ||
}); | ||
## API Reference | ||
let theme = new Theme({colors: [gray, blue, red], backgroundColor: gray, lightness: 97}); | ||
### `generateAdaptiveTheme` | ||
Function used to create a fully adaptive contrast-based color palette/theme using Leonardo. Parameters are destructured and need to be explicitly called, such as `colorKeys: ["#f26322"]`. Parameters can be passed as a config JSON file for modularity and simplicity. | ||
```js | ||
generateAdaptiveTheme({colorScales, baseScale}); // returns function | ||
generateAdaptiveTheme({colorScales, baseScale, brightness}); // returns color objects | ||
// returns theme colors as JSON | ||
let colors = theme.contrastColors; | ||
``` | ||
Returned function: | ||
```js | ||
myTheme(brightness, contrast); | ||
``` | ||
## API Reference | ||
#### `colorScales` *[array of objects]*: | ||
Each object contains the necessary parameters for [generating colors by contrast](#generateContrastColors) with the exception of the `name` and `ratios` parameter. For `generateAdaptiveTheme`, [ratios can be an array or an object](#ratios-array-or-object). | ||
### `Theme` | ||
Example of `colorScales` object with all options: | ||
Class function used to generate adaptive contrast-based colors. Parameters are destructured and need to be explicitly called. | ||
```js | ||
{ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
colorSpace: 'LCH', | ||
ratios: { | ||
'blue--largeText': 3, | ||
'blue--normalText': 4.5 | ||
} | ||
} | ||
``` | ||
| Parameter | Type | Description | | ||
|-----------|-------|------------| | ||
| `colors` | Array | List of `Color` classes to generate theme colors for. A single `BackgroundColor` class is required. | | ||
| `lightness` | Number | Value from 0-100 for desired lightness of generated theme background color (whole number)| | ||
| `contrast` | Number | Multiplier to increase or decrease contrast for all theme colors (default is `1`) | | ||
| `output` | Enum | Desired color output format | | ||
#### `baseScale` *string (enum)*: | ||
String value matching the `name` of a `colorScales` object to be used as a [base scale](#generateBaseScale) (background color). This creates a scale of values from 0-100 in lightness, which is used for `brightness` parameter. Ie. `brightness: 90` returns the 90% lightness value of the base scale. | ||
#### `name` *string*: | ||
Unique name for each color scale. This value refers to the entire color group _(eg "blue")_ and will be used for the output color keys, ie `blue100: '#5CDBFF'` | ||
#### Setters | ||
| Setter | Description of output | | ||
|--------|-----------------------| | ||
| `.lightness()` | Sets the theme's lightness value | | ||
| `.contrast()` | Sets the theme's contrast value | | ||
| `.backgroundColor()` | Sets the theme's background color (creates a new `BackgroundColor` if passing a string) | | ||
| `.colors()` | Sets colors for theme (must pass `Color`)| | ||
| `.output()` | Sets output format for theme | | ||
#### `ratios` *array* or *object*: | ||
List of numbers to be used as target contrast ratios. If entered as an array, swatch names are incremented in `100`s such as `blue100`, `blue200` based on the color scale [name](#name-string). | ||
Alternatively, `ratios` can be an object with custom keys to name each color, such as `['Blue_Large_Text', 'Blue_Normal_Text']`. | ||
#### Supported output formats: | ||
Available output formats conform to the [W3C CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) spec for the supported options, as listed below: | ||
#### `brightness` *number*: | ||
Optional value from 0-100 indicating the brightness of the base / background color. If undefined, `generateAdaptiveTheme` will return a function | ||
#### `contrast` *integer*: | ||
Optional value to increase contrast of your generated colors. This value is multiplied against all ratios defined for each color scale. | ||
#### `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 | | ||
@@ -135,13 +98,104 @@ |---------------|--------------| | ||
---------- | ||
#### 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. | ||
### `Color` | ||
Class function used to define colors for a theme. Parameters are destructured and need to be explicitly called. | ||
Colors with a **positive contrast ratio** with the base (ie, 2:1) will be named in increments of 100. For example, `gray100`, `gray200`. | ||
| Parameter | Type | Description | | ||
|-----------|-------|------| | ||
| `name` | String | User-defined name for a color, (eg "Blue"). Used to name output color values | | ||
| `colorKeys` | Array of strings | List of specific colors to interpolate between in order to generate a full lightness scale of the color. | | ||
| `colorspace` | Enum | The [colorspace](#Supported-interpolation-colorspaces) in which the key colors will be interpolated within. | | ||
| `ratios` | Array or Object | List of target contrast ratios, or object with named keys for each value. | | ||
| `smooth` | Boolean | Applies bezier smoothing to interpolation (false by default) | | ||
| `output` | Enum | Desired color output format | | ||
Colors with a **negative contrast ratio** with the base (ie -2:1) will be named in increments less than 100 and based on the number of negative values declared. For example, if there are 3 negative values `[-1.4, -1.3, -1.2, 1, 2, 3]`, the name for those values will be incremented by 100/4 (length plus one to avoid a `0` value), such as `gray25`, `gray50`, and `gray75`. | ||
#### Setters | ||
| Setter | Description of output | | ||
|--------|-----------------------| | ||
| `.colorKeys()` | Sets the color keys | | ||
| `.colorspace()` | Sets the interpolation colorspace | | ||
| `.ratios()` | Sets the ratios | | ||
| `.name()` | Sets the name | | ||
| `.smooth()` | Sets the smoothing option | | ||
| `.output()` | Sets the output format | | ||
Here is an example output from a theme: | ||
#### Supported interpolation colorspaces: | ||
Below are the available options for interpolation in Leonardo: | ||
- [LCH](https://en.wikipedia.org/wiki/HCL_color_space) | ||
- [LAB](https://en.wikipedia.org/wiki/CIELAB_color_space) | ||
- [CAM02](https://en.wikipedia.org/wiki/CIECAM02) | ||
- [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) | ||
- [HSLuv](https://en.wikipedia.org/wiki/HSLuv) | ||
- [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) | ||
- [RGB](https://en.wikipedia.org/wiki/RGB_color_space) | ||
#### Ratios as an array | ||
When passing a flat array of target ratios, the output colors in your Theme will be generated by concatenating the color name (eg "Blue") with numeric increments. Colors with a **positive contrast ratio** with the base (ie, 2:1) will be named in increments of 100. For example, `gray100`, `gray200`. | ||
Colors with a **negative contrast ratio** with the base (ie -2:1) will be named in increments less than 100 and _based on the number of negative values declared_. For example, if there are 3 negative values `[-1.4, -1.3, -1.2, 1, 2, 3]`, the name for those values will be incremented by 100/4 (length plus one to avoid a `0` value), such as `gray25`, `gray50`, and `gray75`. | ||
For example: | ||
```js | ||
new Color({ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
colorSpace: 'LCH', | ||
ratios: [3, 4.5] | ||
}); | ||
// Returns: | ||
[ | ||
{ | ||
name: 'blue', | ||
values: [ | ||
{name: "blue100", contrast: 3, value: "#8d63ff"}, | ||
{name: "blue200", contrast: 4.5, value: "#623aff"} | ||
] | ||
} | ||
] | ||
``` | ||
#### Ratios as an object | ||
When defining ratios as an object with key-value pairs, you define what name will be output in your Leonardo theme. | ||
```js | ||
new Color({ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
colorSpace: 'LCH', | ||
ratios: { | ||
'blue--largeText': 3, | ||
'blue--normalText': 4.5 | ||
} | ||
}); | ||
// Returns: | ||
[ | ||
{ | ||
name: 'blue', | ||
values: [ | ||
{name: "blue--largeText", contrast: 3, value: "#8d63ff"}, | ||
{name: "blue--normalText", contrast: 4.5, value: "#623aff"} | ||
] | ||
} | ||
] | ||
``` | ||
--- | ||
## Output examples | ||
There are two types of output you can get from the `Theme` class: | ||
| Getter | Description of output | | ||
|--------|-----------------------| | ||
| `Theme.contrastColors` | Returns array of color objects with key-value pairs | | ||
| `Theme.contrastColorValues` | Returns flat array of color values | | ||
### `.contrastColors` output | ||
Each color instance is named by concatenating the user-defined color name with a numeric value (eg `name: 'gray'`; `gray100`). | ||
Example output: | ||
```js | ||
[ | ||
{ background: "#e0e0e0" }, | ||
@@ -169,44 +223,25 @@ { | ||
#### Examples | ||
###### Creating your theme as a function | ||
### `.contrastColorValues` output | ||
For the same example theme shown above, these values would be returned in a flat array when calling `Theme.contrastColorValues`. | ||
Example output: | ||
```js | ||
let myPalette = { | ||
colorScales: [ | ||
{ | ||
name: 'gray', | ||
colorKeys: ['#cacaca'], | ||
colorspace: 'HSL', | ||
ratios: [1, 2, 3, 4.5, 8, 12] | ||
}, | ||
{ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
colorspace: 'HSL', | ||
ratios: [3, 4.5] | ||
}, | ||
{ | ||
name: 'red', | ||
colorKeys: ['#FF9A81', '#FF0000'], | ||
colorspace: 'HSL', | ||
ratios: [3, 4.5] | ||
} | ||
], | ||
baseScale: 'gray' | ||
} | ||
let myTheme = generateAdaptiveTheme(myPalette); | ||
myTheme(95, 1.2) // outputs colors with background lightness of 95 and ratios increased by 1.2 | ||
[ | ||
"#e0e0e0", | ||
"#a0a0a0", | ||
"#808080", | ||
"#646464", | ||
"#b18cff", | ||
"#8d63ff", | ||
"#623aff", | ||
"#1c0ad1" | ||
] | ||
``` | ||
###### Creating static instances of your theme | ||
```js | ||
// theme on light gray | ||
let lightTheme = generateAdaptiveTheme(95); | ||
--- | ||
// theme on dark gray with increased contrast | ||
let darkTheme = generateAdaptiveTheme(20, 1.3); | ||
``` | ||
## Leonardo with CSS variables | ||
Here are a few examples of how you can utilize Leonardo to dynamically create or modify CSS variables for your application. | ||
###### Assigning output to CSS properties | ||
### Vanilla JS | ||
```js | ||
@@ -231,41 +266,169 @@ let varPrefix = '--'; | ||
### generateContrastColors | ||
### React | ||
Create a new Theme component `Theme.js` with your parameters: | ||
```js | ||
import * as Leo from '@adobe/leonardo-contrast-colors'; | ||
Primary function used to generate colors based on target contrast ratios. Parameters are destructured and need to be explicitly called, such as `colorKeys: ["#f26322"]`. | ||
const Theme = () => { | ||
let gray = new Leo.BackgroundColor({ | ||
name: 'gray', | ||
colorKeys: ['#cacaca'], | ||
ratios: [2, 3, 4.5, 8] | ||
}); | ||
let blue = new Leo.Color({ | ||
name: 'blue', | ||
colorKeys: ['#5CDBFF', '#0000FF'], | ||
ratios: [3, 4.5] | ||
}); | ||
let red = new Leo.Color({ | ||
name: 'red', | ||
colorKeys: ['#FF9A81', '#FF0000'], | ||
ratios: [3, 4.5] | ||
}); | ||
const adaptiveTheme = new Leo.Theme({ | ||
colors: [ | ||
gray, | ||
blue, | ||
red | ||
], | ||
backgroundColor: gray, | ||
lightness: 97, | ||
contrast: 1, | ||
}); | ||
return adaptiveTheme; | ||
} | ||
export default Theme; | ||
``` | ||
Then import your Theme component at the top level of your application, and pass the Theme as a property of your app: | ||
```js | ||
generateContrastColors({colorKeys, base, ratios, colorspace}) | ||
// index.js | ||
import Theme from './components/Theme'; | ||
ReactDOM.render( | ||
<React.StrictMode> | ||
<App adaptiveTheme={Theme()}/> | ||
</React.StrictMode>, | ||
document.getElementById('root') | ||
); | ||
``` | ||
#### `colorKeys` *[array]*: | ||
List of colors referenced to generate a lightness scale. Much like [key frames](https://en.wikipedia.org/wiki/Key_frame), key colors are single points by which additional colors will be interpolated between. | ||
In your App.js file, import `useTheme` from `css-vars-hook` and provide the following within your App function in order to format Leonardo's output in the structure required for `css-vars-hook`. | ||
```js | ||
// App.js | ||
import {useTheme} from 'css-vars-hook'; | ||
#### `base` *string*: | ||
References the color value that the color is to be generated from. | ||
function App(props) { | ||
const [lightness, setLightness] = useState(100); | ||
const [contrast, setContrast] = useState(1); | ||
#### `ratios` *[array]*: | ||
List of numbers to be used as target contrast ratios. | ||
const _createThemeObject = () => { | ||
let themeObj = {} | ||
props.adaptiveTheme.contrastColors.forEach(color => { | ||
if(color.name) { | ||
let values = color.values; | ||
values.forEach(instance => { | ||
let name = instance.name; | ||
let val = instance.value; | ||
themeObj[name] = val; | ||
}); | ||
} else { | ||
// must be the background | ||
let name = 'background' | ||
let val = color.background; | ||
themeObj[name] = val; | ||
} | ||
}) | ||
return themeObj; | ||
}; | ||
#### `colorspace` *string*: | ||
The colorspace in which the key colors will be interpolated within. Below are the available options: | ||
const theme = useState( _createThemeObject() ); | ||
- [LCH](https://en.wikipedia.org/wiki/HCL_color_space) | ||
- [LAB](https://en.wikipedia.org/wiki/CIELAB_color_space) | ||
- [CAM02](https://en.wikipedia.org/wiki/CIECAM02) | ||
- [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) | ||
- [HSLuv](https://en.wikipedia.org/wiki/HSLuv) | ||
- [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) | ||
- [RGB](https://en.wikipedia.org/wiki/RGB_color_space) | ||
const {setRef, setVariable} = useTheme(theme); | ||
### generateBaseScale | ||
return ( | ||
<div | ||
className="App" | ||
ref={setRef} | ||
> | ||
</div> | ||
) | ||
} | ||
This function is used to generate a color scale tailored specifically for use as a brightness scale when using Leonardo for brightness and contrast controls. Colors are generated that match HSLuv lightness values from `0` to `100` and are output as hex values. | ||
``` | ||
To make your application adaptive, include a function for updating your theme before your return function: | ||
```js | ||
generateBaseScale({colorKeys, colorspace}) | ||
function _updateColorVariables() { | ||
let themeInstance = _createThemeObject(); | ||
for (const [key, value] of Object.entries( themeInstance )) { | ||
setVariable(key, value); | ||
} | ||
}; | ||
// call function to set initial values | ||
_updateColorVariables(); | ||
``` | ||
Only accepts **colorKeys** and **colorspace** parameters, as defined above for [`generateContrastColors`](#generateContrastColors) | ||
Finally, reference this function and set the theme parameters when your users interact with slider components (do the same for Contrast): | ||
```js | ||
<label htmlFor="lightness"> | ||
Lightness | ||
</label> | ||
<input | ||
value={lightness} | ||
id="lightness" | ||
type="range" | ||
min={ sliderMin } | ||
max={ sliderMax } | ||
step="1" | ||
onChange={e => { | ||
setLightness(e.target.value) | ||
props.adaptiveTheme.lightness = e.target.value | ||
_updateColorVariables() | ||
}} | ||
/> | ||
<label htmlFor="contrast"> | ||
Contrast | ||
</label> | ||
<input | ||
value={contrast} | ||
id="contrast" | ||
type="range" | ||
min="0.25" | ||
max="3" | ||
step="0.025" | ||
onChange={e => { | ||
setContrast(e.target.value) | ||
props.adaptiveTheme.contrast = e.target.value | ||
_updateColorVariables() | ||
}} | ||
/> | ||
``` | ||
### Dark mode support in React | ||
Include the following in your App.js file to listen for dark mode. This will pass a different lightness value (of your choice) to Leonardo. It's recommended to restrict the lightness range based on mode in order to avoid inaccessible ranges and to provide a better overall experience | ||
```js | ||
const mq = window.matchMedia('(prefers-color-scheme: dark)'); | ||
// Update lightness and slider min/max to be conditional: | ||
const [lightness, setLightness] = useState((mq.matches) ? 8 : 100); | ||
const [sliderMin, setSliderMin] = useState((mq.matches) ? 0 : 80); | ||
const [sliderMax, setSliderMax] = useState((mq.matches) ? 30 : 100); | ||
// Listener to update when user device mode changes: | ||
mq.addEventListener('change', function (evt) { | ||
props.adaptiveTheme.lightness = ((mq.matches) ? 11 : 100) | ||
setLightness((mq.matches) ? 11 : 100) | ||
setSliderMin((mq.matches) ? 0 : 80); | ||
setSliderMax((mq.matches) ? 30 : 100); | ||
}); | ||
``` | ||
--- | ||
## Why are not all contrast ratios available? | ||
@@ -288,2 +451,5 @@ You may notice the tool takes an input (target ratio) but most often outputs a contrast ratio slightly higher. This has to do with the available colors in the RGB color space, and the math associated with calculating these ratios. | ||
--- | ||
## D3 Color | ||
@@ -290,0 +456,0 @@ This project is currently built using [D3 color](https://github.com/d3/d3-color). Although functionality is comparable to [Chroma.js](https://gka.github.io/chroma.js/), the choice of D3 color is based on the additional modules available for state-of-the-art [color appearance models](https://en.wikipedia.org/wiki/Color_appearance_model), such as [CIE CAM02](https://gramaz.io/d3-cam02/). |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
88455
1813
469
1
+ Addedd3-color@^3.0.1
+ Addedd3-interpolate@^3.0.1
+ Addedd3-color@3.1.0(transitive)
+ Addedd3-interpolate@3.0.1(transitive)
- Removedd3@^5.14.2
- Removedd3-3d@0.0.9
- Removedcommander@2.20.3(transitive)
- Removedd3@5.16.0(transitive)
- Removedd3-3d@0.0.9(transitive)
- Removedd3-array@1.2.4(transitive)
- Removedd3-axis@1.0.12(transitive)
- Removedd3-brush@1.1.6(transitive)
- Removedd3-chord@1.0.6(transitive)
- Removedd3-collection@1.0.7(transitive)
- Removedd3-contour@1.3.2(transitive)
- Removedd3-dispatch@1.0.6(transitive)
- Removedd3-drag@1.2.5(transitive)
- Removedd3-dsv@1.2.0(transitive)
- Removedd3-ease@1.0.7(transitive)
- Removedd3-fetch@1.2.0(transitive)
- Removedd3-force@1.2.1(transitive)
- Removedd3-format@1.4.5(transitive)
- Removedd3-geo@1.12.1(transitive)
- Removedd3-hierarchy@1.1.9(transitive)
- Removedd3-interpolate@1.4.0(transitive)
- Removedd3-path@1.0.9(transitive)
- Removedd3-polygon@1.0.6(transitive)
- Removedd3-quadtree@1.0.7(transitive)
- Removedd3-random@1.1.2(transitive)
- Removedd3-scale@2.2.2(transitive)
- Removedd3-scale-chromatic@1.5.0(transitive)
- Removedd3-selection@1.4.2(transitive)
- Removedd3-shape@1.3.7(transitive)
- Removedd3-time@1.1.0(transitive)
- Removedd3-time-format@2.3.0(transitive)
- Removedd3-timer@1.0.10(transitive)
- Removedd3-transition@1.3.2(transitive)
- Removedd3-voronoi@1.1.4(transitive)
- Removedd3-zoom@1.8.3(transitive)
- Removediconv-lite@0.4.24(transitive)
- Removedrw@1.3.3(transitive)
- Removedsafer-buffer@2.1.2(transitive)