diff --git a/docs/_data/palettes.js b/docs/_data/palettes.js index c16e54f75..3ce705beb 100644 --- a/docs/_data/palettes.js +++ b/docs/_data/palettes.js @@ -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'; diff --git a/src/styles/color/palettes-analyzed.js b/src/styles/color/scripts/palettes-analyzed.js similarity index 95% rename from src/styles/color/palettes-analyzed.js rename to src/styles/color/scripts/palettes-analyzed.js index 88640201d..45430a753 100644 --- a/src/styles/color/palettes-analyzed.js +++ b/src/styles/color/scripts/palettes-analyzed.js @@ -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 }; diff --git a/src/styles/color/palettes.js b/src/styles/color/scripts/palettes.js similarity index 85% rename from src/styles/color/palettes.js rename to src/styles/color/scripts/palettes.js index a995d8588..1507b58d0 100644 --- a/src/styles/color/palettes.js +++ b/src/styles/color/scripts/palettes.js @@ -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-(?[a-z]+)-(?[0-9]+):\s*(?.+?)\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$/, ''); diff --git a/src/styles/color/scripts/ranges.js b/src/styles/color/scripts/ranges.js new file mode 100644 index 000000000..f703fd95d --- /dev/null +++ b/src/styles/color/scripts/ranges.js @@ -0,0 +1,98 @@ +// Run via node ranges.js to analyze all palettes +// or node ranges.js 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); +} diff --git a/src/styles/color/tintless.js b/src/styles/color/scripts/tintless.js similarity index 68% rename from src/styles/color/tintless.js rename to src/styles/color/scripts/tintless.js index 3fb14bc37..4277a2400 100644 --- a/src/styles/color/tintless.js +++ b/src/styles/color/scripts/tintless.js @@ -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; -} diff --git a/src/styles/color/scripts/util.js b/src/styles/color/scripts/util.js new file mode 100644 index 000000000..723d083e1 --- /dev/null +++ b/src/styles/color/scripts/util.js @@ -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); +}