colorally
Version:
Name colors by well-known definitions
241 lines (199 loc) • 8.64 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var chalk = require('chalk');
var clipboardy = require('clipboardy');
var colors = require('../data/colors.json');
var deltaE = require('delta-e');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var chalk__default = /*#__PURE__*/_interopDefaultLegacy(chalk);
var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors);
var version = "2.0.8";
// ╻ ╻╺┳╸╻╻ ╻╺┳╸╻ ╻
// ┃ ┃ ┃ ┃┃ ┃ ┃ ┗┳┛
// ┗━┛ ╹ ╹┗━╸╹ ╹ ╹
const duplicate = x => x + x;
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const flatten = arr => arr.reduce((acc, v) => acc.concat(Array.isArray(v) ? flatten(v) : v), []);
const isBoolean = val => typeof val === 'boolean';
const isEmpty = x => x == null || !Object.keys(x).length;
const isString = x => typeof x === 'string';
const map = fn => arr => arr.map(fn);
const take = n => arr => arr.slice(0, n);
const split = x => str => str.split(x);
const toCase = convertWord => separator => str => str.toLowerCase().split(/\s+/).map(convertWord).join(separator);
const toCamelCase = toCase(([head, ...tail], idx) => idx === 0 ? [head, ...tail].join('') : head.toUpperCase() + tail.join(''))('');
const toConstantCase = toCase(word => word.toUpperCase())('_');
const toDotCase = toCase(word => word.toLowerCase())('.');
const toKebabCase = toCase(word => word.toLowerCase())('-');
const toLowerCase = toCase(word => word.toLowerCase())(' ');
const toPascalCase = toCase(([head, ...tail]) => head.toUpperCase() + tail.join(''))('');
const toSnakeCase = toCase(word => word.toLowerCase())('_');
const toTitleCase = toCase(([head, ...tail]) => head.toUpperCase() + tail.join(''))(' ');
const toUpperCase = toCase(word => word.toUpperCase())(' ');
const zipObj = ks => vs => ks.reduce((acc, k, idx) => ({ ...acc,
[k]: vs[idx]
}), {});
// ┏━╸┏━┓┏┓╻╻ ╻┏━╸┏━┓╺┳╸┏━╸┏━┓┏━┓
const omitTypeIndicators = str => str.replace(/^#|0x/, '');
const expandShortHex = str => str.length === 3 ? Array.from(str).map(duplicate).join('') : str;
const convertStringToHex = str => parseInt(str, 16);
const rgbToXyz = rgb => {
const [r, g, b] = rgb.map(x => {
const y = x / 255;
return y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92;
});
return [(r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047, (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.0, (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883].map(x => x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116);
};
const xyzToLab = ([x, y, z]) => [116 * y - 16, 500 * (x - y), 200 * (y - z)];
const hexToRgb = hex => {
const r = hex >> 16;
const g = hex >> 8 & 0xff;
const b = hex & 0xff;
return [r, g, b];
};
const strToHex = compose(convertStringToHex, expandShortHex, take(6), omitTypeIndicators);
const rgbToLab = compose(xyzToLab, rgbToXyz);
// ┏━┓╻ ╻╻ ┏━╸┏━┓
const measureDistance = (...rgbs) => deltaE.getDeltaE00(...rgbs.map(compose(zipObj(['L', 'A', 'B']), rgbToLab)));
const findSimilarDefinition = definitions => rgb => {
const getNearestDefinition = nearest => current => nearest == null || nearest.distance > current.distance ? current : nearest;
const traverseDefinitions = (nearest, idx = 0) => {
if (idx === definitions.length) return nearest;
const def = definitions[idx];
const current = { ...def,
distance: measureDistance(rgb, def.rgb)
};
return current.distance === 0 ? current : traverseDefinitions(getNearestDefinition(nearest)(current), idx + 1);
};
return traverseDefinitions();
};
// ┏━╸┏━┓╻ ┏━┓┏━┓┏━┓╻ ╻ ╻ ╻
/**
* Find visually similar color definition.
*
* @param {(string|number|number[])} color A color value in hex-string, hex or RGB array.
* @returns {object} A color definition object with name, rgb array and optional match statistics.
*/
function colorally(color) {
const rgb = Array.isArray(color) ? color.map(Number) : hexToRgb(isString(color) ? strToHex(color) : color);
return findSimilarDefinition(colors__default["default"])(rgb);
}
// ┏━╸╻ ╻
const {
argv,
env
} = process;
const defaults = {
options: {
copy: false,
format: false,
help: false,
verbose: false,
version: false
},
values: []
};
const optionDefinitions = [{
name: 'copy',
identifier: /^--?c(opy)?$/
}, {
name: 'format',
identifier: /^--?f(ormat)?(=\w+)?$$/,
input: /^\w+$/
}, {
name: 'help',
identifier: /^--?h(elp)?$/
}, {
name: 'verbose',
identifier: /^--?v(erbose)?$/
}, {
name: 'version',
identifier: /^(-V)|(--version)$/
}];
const printHelp = exitCode => {
console.log(chalk__default["default"]`{blue colorally} - Name colors by well-known definitions
{bold USAGE}
{blue colorally} [option...] [{underline hex}]
{blue colorally} [option...] [{underline red}] [{underline green}] [{underline blue}]
The {underline hex} argument can contain either {bold 3} or {bold 6 digits} ` + chalk__default["default"]`with an optional leading
{bold 0x} or {bold #}.
The {underline red}, {underline green} and {underline blue} arguments may only ` + chalk__default["default"]`contain digits.
{bold OPTIONS}
-V, --version output version number
-c, --copy copy definition to clipboard
-f, --format {underline case} output definition in specified format
-h, --help output usage information
-v, --verbose provide a more talkative result
The {underline case} argument can be either {bold camel} {italic fooBar}, ` + chalk__default["default"]`{bold constant} {italic FOO_BAR}, {bold dot} {italic foo.bar},
{bold kebab} {italic foo-bar}, {bold lower} {italic foo bar}, {bold pascal} ` + chalk__default["default"]` {italic FooBar}, {bold snake} {italic foo_bar} {bold title} {italic Foo Bar} or
{bold upper} {italic FOO BAR}. By default, {blue colorally} sets {underline case} as {bold title}.
`);
return process.exit(exitCode);
};
const printVersion = () => {
return console.log('v' + version);
};
const findOption = str => optionDefinitions.find(opt => opt.identifier.test(str));
const parseArgs = args => {
const omitFalsyValues = arr => arr.filter(Boolean);
const parse = acc => idx => arr => {
const current = arr[idx];
const next = arr[idx + 1] || false;
const opt = findOption(current);
const optValue = opt && opt.input ? opt.input.test(next) ? next : false : true;
const increment = isBoolean(optValue) ? 1 : 2;
if (idx >= arr.length) return acc;
return parse(opt != null ? {
options: { ...acc.options,
[opt.name]: optValue
},
values: acc.values
} : {
options: acc.options,
values: [...acc.values, current]
})(idx + increment)(arr);
};
const parsedArgs = compose(parse(defaults)(0), flatten, map(split('=')))(args);
return { ...parsedArgs,
values: omitFalsyValues(parsedArgs.values)
};
};
const formatDefinitionName = format => name => {
const formatMap = {
camel: toCamelCase,
constant: toConstantCase,
dot: toDotCase,
kebab: toKebabCase,
lower: toLowerCase,
pascal: toPascalCase,
snake: toSnakeCase,
title: toTitleCase,
upper: toUpperCase
};
const validFormat = format && Object.keys(formatMap).find(key => key === format.toLowerCase()) || 'title';
return formatMap[validFormat](name);
};
const run = args => {
const {
options,
values
} = parseArgs(args);
if (options.help) return printHelp(0);
if (options.version) return printVersion();
if (isEmpty(values)) return printHelp(1);
const definition = colorally(values.length > 2 ? values.slice(0, 3) : values[0]);
const name = formatDefinitionName(options.format)(definition.name);
if (options.copy) {
clipboardy.writeSync(name);
}
const colorBlock = // Only display if terminal supports truecolor.
chalk__default["default"].level > 2 ? chalk__default["default"].bgRgb(...definition.rgb)(' ') + ' ' : '';
return console.log(options.verbose ? // prettier-ignore
chalk__default["default"]`Found ${colorBlock}{bold ${name}} {italic (${definition.rgb.join(', ')})} ` + chalk__default["default"]`by a distance of {italic ${definition.distance.toFixed(4)}}.` : name);
}; // Exclude runner from test coverage.
// istanbul ignore next
if (env.NODE_ENV !== 'test') {
run(argv.slice(2, argv.length));
}
exports.run = run;
;