Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@adobe/leonardo-contrast-colors

Package Overview
Dependencies
Maintainers
18
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@adobe/leonardo-contrast-colors - npm Package Compare versions

Comparing version 1.0.0-alpha.8 to 1.0.0-alpha.9

8

CHANGELOG.md

@@ -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 @@

2

curve.js

@@ -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 @@ } | {

@@ -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"
}

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc