Analyze color components (#732)

Also refactored existing color scripts (moved to separate directory, extracted utils to separate file)
This commit is contained in:
Lea Verou
2025-02-10 13:54:54 -05:00
committed by GitHub
parent b6620ddf7e
commit d25f3748c4
6 changed files with 164 additions and 64 deletions

View File

@@ -1 +1 @@
export { default as default } from '../../src/styles/color/palettes-analyzed.js';
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';

View File

@@ -5,6 +5,7 @@
* More later.
*/
import rawPalettes from './palettes.js';
import { clamp } from './util.js';
// Default accent tint if all chromas are 0, but also the tint accent colors will be nudged towards (see chromaTolerance)
const DEFAULT_ACCENT = 60;
@@ -58,9 +59,5 @@ for (let paletteId in palettes) {
}
}
function clamp(min, value, max) {
return Math.min(Math.max(min, value), max);
}
export default palettes;
export { rawPalettes };

View File

@@ -6,11 +6,9 @@
import Color from 'colorjs.io';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { PALETTE_DIR } from './util.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export const paletteFiles = fs.readdirSync(__dirname).filter(file => file.endsWith('.css'));
export const paletteFiles = fs.readdirSync(PALETTE_DIR + '/').filter(file => file.endsWith('.css'));
export const declarationRegex =
/^\s*--wa-color-(?<hue>[a-z]+)-(?<level>[0-9]+):\s*(?<color>.+?)\s*(\/\*.+?\*\/)?\s*;$/gm;
export const rawCSS = {};
@@ -55,7 +53,7 @@ function parse(contents, file) {
const palettes = {};
for (let file of paletteFiles) {
let css = fs.readFileSync(path.join(__dirname, file), 'utf8');
let css = fs.readFileSync(path.join(PALETTE_DIR, file), 'utf8');
rawCSS[file] = css;
let tokens = parse(css, file);
let paletteId = file.replace(/\.css$/, '');

View File

@@ -0,0 +1,98 @@
// Run via node ranges.js to analyze all palettes
// or node ranges.js <paletteId> to analyze a single palette
import palettes from './palettes-analyzed.js';
import { toPrecision } from './util.js';
let paletteId = process.argv[2];
/**
* Each "test" consists of the following params to analyze:
* - component: The color component to analyze (h, c, l)
* - label: The label to display in the console
* - by: The grouping to analyze by (tint, hue)
* - levels: The number of tints from the core color to include in the analysis.
* Examples: undefined for all tints, 0 for the core color only, 10 for the core color and ±10 from it.
*/
let tests = [
{ component: 'h', label: 'Hue', by: 'hue', levels: paletteId ? undefined : 10 },
{ component: 'c', label: 'Chroma', by: 'tint' },
{ component: 'l', label: 'L', by: 'tint' },
];
if (!paletteId) {
tests.push({ component: 'h', label: 'Core Hue', by: 'hue', levels: 0 });
}
const tints = ['95', '90', '80', '70', '60', '50', '40', '30', '20', '10', '05'];
const hues = ['red', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'gray'];
function analyzePalette(scales, results, { component, levels, by = 'tint' }) {
for (let hue in scales) {
let colors = scales[hue];
let key = colors.maxChromaTint;
let resultsByHue = by === 'hue' ? results[hue] : results;
for (let tint of tints) {
let color = colors[tint];
let value = color[component];
let resultsByTint = by === 'tint' ? resultsByHue[tint] : resultsByHue;
if (levels === undefined || Math.abs(tint - key) <= levels) {
if (resultsByTint.min > value) resultsByTint.min = value;
if (resultsByTint.max < value) resultsByTint.max = value;
}
}
}
}
function analyze(options = {}) {
let results = {};
let keys = options.by === 'hue' ? hues : tints;
for (let key of keys) {
results[key] = { min: Infinity, max: -Infinity };
}
if (paletteId) {
analyzePalette(palettes[paletteId], results, options);
} else {
for (let paletteId in palettes) {
analyzePalette(palettes[paletteId], results, options);
}
}
// Add extent & mid, make numbers easier to read
for (let key of keys) {
let info = results[key];
if (options.component === 'h') {
// Fixup hues crossing 0
if (Math.abs(info.max - info.min) > 180) {
info.min += 360;
if (info.min > info.max) {
[info.min, info.max] = [info.max, info.min];
}
}
}
info.extent = info.max - info.min;
info.mid = (info.min + info.max) / 2;
for (let prop in info) {
info[prop] = toPrecision(info[prop]);
}
}
let label = `${options.label || options.component} ranges`;
console.log(label + (options.levels !== undefined ? `${options.levels} from core tint)` : '') + ':');
console.table(results);
}
if (paletteId) {
// Analyze a single palette
console.log(`Analyzing palette '${paletteId}'`);
}
for (let test of tests) {
analyze(test);
}

View File

@@ -6,23 +6,14 @@
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import palettes, { rawPalettes } from './palettes-analyzed.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
import { PALETTE_DIR, formatComparison, hueToChalk } from './util.js';
const selector = paletteId =>
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${paletteId}`].join(
',\n',
);
// Default accent tint if all chromas are 0, but also the tint accent colors will be nudged towards (see chromaTolerance)
const defaultAccent = 60;
// Min and max allowed tints
const minAccentTint = 40;
const maxAccentTint = 90;
// Used for formatting warnings
const paletteIdMaxChars = Object.keys(palettes).reduce((max, id) => Math.max(max, id.length), 0);
const hueMaxChars = Object.keys(palettes.default).reduce((max, id) => Math.max(max, id.length), 0);
@@ -80,53 +71,10 @@ for (let paletteId in palettes) {
let indent = ' ';
css = `${selector(paletteId)} {\n${css.trimEnd().replace(/^(?=\S)/gm, indent)}\n}\n`;
fs.writeFileSync(path.join(__dirname, paletteId + '.css'), css, 'utf8');
fs.writeFileSync(path.join(PALETTE_DIR, paletteId + '.css'), css, 'utf8');
}
console.info(
`🎨 Wrote ${Object.keys(palettes).length} palette files.` +
(issueCount > 0 ? ` ${chalk.bold(issueCount)} issues found across ${chalk.bold(issuePaletteCount)} palettes.` : ''),
);
/**
* Format a comparison by rounding numbers to the lowest number of significant digits that still shows a difference.
* @param {number} a
* @param {number} b
* @returns {string}
*/
function formatComparison(a, b) {
let op = a < b ? '<' : '>';
for (let i = 1; i < 10; i++) {
let roundedA = a.toPrecision(i);
let roundedB = b.toPrecision(i);
if (roundedA !== roundedB) {
return `${roundedA} ${op} ${roundedB}`;
}
}
return `${a} ${op} ${b}`;
}
function hueToChalk(hue) {
let ret;
if (hue in chalk) {
ret = chalk[hue];
}
switch (hue) {
case 'indigo':
ret = chalk.hex('#8a8beb');
break;
case 'purple':
ret = chalk.hex('#a94dc6');
break;
}
if (ret) {
return ret.bold;
}
return chalk.bold;
}

View File

@@ -0,0 +1,59 @@
import chalk from 'chalk';
import path from 'path';
import { fileURLToPath } from 'url';
let url = new URL('.', import.meta.url);
// One level up
url.pathname = path.join(url.pathname, '..');
export const PALETTE_DIR = fileURLToPath(url);
export function clamp(min, value, max) {
return Math.min(Math.max(min, value), max);
}
/**
* Format a comparison by rounding numbers to the lowest number of significant digits that still shows a difference.
* @param {number} a
* @param {number} b
* @returns {string}
*/
export function formatComparison(a, b) {
let op = a < b ? '<' : '>';
for (let i = 1; i < 10; i++) {
let roundedA = a.toPrecision(i);
let roundedB = b.toPrecision(i);
if (roundedA !== roundedB) {
return `${roundedA} ${op} ${roundedB}`;
}
}
return `${a} ${op} ${b}`;
}
export function hueToChalk(hue) {
let ret;
if (hue in chalk) {
ret = chalk[hue];
}
switch (hue) {
case 'indigo':
ret = chalk.hex('#8a8beb');
break;
case 'purple':
ret = chalk.hex('#a94dc6');
break;
}
if (ret) {
return ret.bold;
}
return chalk.bold;
}
export function toPrecision(value, precision = 2) {
return +Number(value).toPrecision(precision);
}