diff --git a/package-lock.json b/package-lock.json index 43b49fb..b133ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@priduck-color-theme/cli": "^0.0.1", + "colorjs.io": "^0.5.0", "yargs": "^17.7.2" }, "devDependencies": { @@ -79,6 +80,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/colorjs.io": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.0.tgz", + "integrity": "sha512-qekjTiBLM3F/sXKks/ih5aWaHIGu+Ftel0yKEvmpbKvmxpNOhojKgha5uiWEUOqEpRjC1Tq3nJRT7WgdBOxIGg==" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index 6fa9ef3..cde3d22 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@priduck-color-theme/cli": "^0.0.1", + "colorjs.io": "^0.5.0", "yargs": "^17.7.2" } } diff --git a/src/cli.mjs b/src/cli.mjs index f08b089..fb4dda8 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -1,12 +1,14 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import * as cssCmd from "./cmds/css.mjs"; +import * as paletteCmd from "./cmds/palette.mjs"; // Set up the top-level command. yargs(hideBin(process.argv)) .scriptName("priduck") .usage("$0 [args]") .command(cssCmd) + .command(paletteCmd) .demand(2) .help() .parse(); diff --git a/src/cmds/css/variables.mjs b/src/cmds/css/variables.mjs index 8a30496..5c9a42d 100644 --- a/src/cmds/css/variables.mjs +++ b/src/cmds/css/variables.mjs @@ -1,25 +1,18 @@ import { mkdirp } from "mkdirp"; -import { fileURLToPath } from "url"; import path from "path"; import fs from "node:fs/promises"; +import * as colors from "../../colors.mjs"; export const command = "variables [path.css]"; export const desc = "Generate a CSS file that uses variables to generate"; export function builder(argv) { - argv - .option("c", { - alias: "hue", - default: 220, - describe: "The hue (0-360) for the base color", - type: "number", - }) - .option("o", { - alias: "output", - demandOption: true, - describe: "The output path for colors.css", - type: "string", - }); + argv.option("o", { + alias: "output", + demandOption: true, + describe: "The output path for colors.css", + type: "string", + }); } export async function handler(argv) { @@ -36,19 +29,19 @@ export async function handler(argv) { // We have ten colors, with 0 being the base hue and the others being evenly // rotated around the color wheel. - for (let color = 0; color < 10; color++) { + for (const color of colors.colorList) { // Figure out the rotation. - const rotation = color * 36; // 1/10th of a circle + const rotation = colors.getHue(color); // For each hue, we have ten levels of brightness ranging from very dark // to very light. - for (let brightness = 0; brightness < 10; brightness++) { + for (const brightness of colors.brightnessList) { // We use a standard code (--color-cXbY) for our codes. const code = `--color-priduck-c${color}b${brightness}`; // Figure out the ramps we are using for brighteness. - const l = brightness * 11; - const s = 50 - brightness * 5; + const l = colors.getLightness(brightness); + const s = colors.getChroma(brightness); // Write out the CSS. lines.push( diff --git a/src/cmds/palette.mjs b/src/cmds/palette.mjs new file mode 100644 index 0000000..1422f07 --- /dev/null +++ b/src/cmds/palette.mjs @@ -0,0 +1,79 @@ +import { mkdirp } from "mkdirp"; +import path from "path"; +import fs from "node:fs/promises"; +import * as colors from "../colors.mjs"; + +export const command = "palette [path.gpl]"; +export const desc = "Generate a GNU Imp/Inkscape palette"; + +export function builder(argv) { + argv + .option("c", { + alias: "hue", + default: 220, + describe: "The hue (0-360) for the base color", + type: "number", + }) + .option("s", { + alias: "secondary", + default: 5, + choices: colors.colorList, + describe: "The color code (0-9) based on the hue", + type: "number", + }) + .option("t", { + alias: "tertiary", + default: 8, + choices: colors.colorList, + describe: "The color code (0-9) based on the hue", + type: "number", + }) + .option("n", { + alias: "name", + default: "Priduck", + describe: "The name of the palette", + type: "string", + }) + .option("o", { + alias: "output", + demandOption: true, + describe: "The output path for colors.css", + type: "string", + }); +} + +export async function handler(argv) { + // Figure out where we need to write the files. + const file = argv.output; + const dir = path.dirname(file); + + console.log("base hue", argv.hue); + console.log("writing", file); + + await mkdirp(dir); + + // Start with the header. + let lines = ["GIMP Palette", `Name: ${argv.name}`, "Columns: 30", ""]; + + // Loop through the three primary colors, then the brightness for each one. + const colorList = [0, argv.secondary, argv.tertiary]; + + for (const colorIndex in colorList) { + const color = colorList[colorIndex]; + const colorName = + colorIndex == 0 ? "primary" : colorIndex == 1 ? "secondary" : "tertiary"; + + for (const brightness of colors.brightnessList) { + const lch = colors.getLchCss(argv.hue, color, brightness); + const rgb = colors.getRgbArray(argv.hue, color, brightness); + const line = `${rgb[0]} ${rgb[1]} ${rgb[2]} ${colorName}-${brightness} # ${lch}`; + + lines.push(line); + } + } + + // Write out the files. + await fs.writeFile(file, Buffer.from(lines.join("\n"))); + + console.log("wrote", file); +} diff --git a/src/colors.css b/src/colors.css deleted file mode 100644 index f053e54..0000000 --- a/src/colors.css +++ /dev/null @@ -1,102 +0,0 @@ -:root { - --color-c0b0: lch(0 50 calc(var(--color-hue) + 0)); - --color-c0b1: lch(11 45 calc(var(--color-hue) + 0)); - --color-c0b2: lch(22 40 calc(var(--color-hue) + 0)); - --color-c0b3: lch(33 35 calc(var(--color-hue) + 0)); - --color-c0b4: lch(44 30 calc(var(--color-hue) + 0)); - --color-c0b5: lch(55 25 calc(var(--color-hue) + 0)); - --color-c0b6: lch(66 20 calc(var(--color-hue) + 0)); - --color-c0b7: lch(77 15 calc(var(--color-hue) + 0)); - --color-c0b8: lch(88 10 calc(var(--color-hue) + 0)); - --color-c0b9: lch(99 5 calc(var(--color-hue) + 0)); - --color-c1b0: lch(0 50 calc(var(--color-hue) + 36)); - --color-c1b1: lch(11 45 calc(var(--color-hue) + 36)); - --color-c1b2: lch(22 40 calc(var(--color-hue) + 36)); - --color-c1b3: lch(33 35 calc(var(--color-hue) + 36)); - --color-c1b4: lch(44 30 calc(var(--color-hue) + 36)); - --color-c1b5: lch(55 25 calc(var(--color-hue) + 36)); - --color-c1b6: lch(66 20 calc(var(--color-hue) + 36)); - --color-c1b7: lch(77 15 calc(var(--color-hue) + 36)); - --color-c1b8: lch(88 10 calc(var(--color-hue) + 36)); - --color-c1b9: lch(99 5 calc(var(--color-hue) + 36)); - --color-c2b0: lch(0 50 calc(var(--color-hue) + 72)); - --color-c2b1: lch(11 45 calc(var(--color-hue) + 72)); - --color-c2b2: lch(22 40 calc(var(--color-hue) + 72)); - --color-c2b3: lch(33 35 calc(var(--color-hue) + 72)); - --color-c2b4: lch(44 30 calc(var(--color-hue) + 72)); - --color-c2b5: lch(55 25 calc(var(--color-hue) + 72)); - --color-c2b6: lch(66 20 calc(var(--color-hue) + 72)); - --color-c2b7: lch(77 15 calc(var(--color-hue) + 72)); - --color-c2b8: lch(88 10 calc(var(--color-hue) + 72)); - --color-c2b9: lch(99 5 calc(var(--color-hue) + 72)); - --color-c3b0: lch(0 50 calc(var(--color-hue) + 108)); - --color-c3b1: lch(11 45 calc(var(--color-hue) + 108)); - --color-c3b2: lch(22 40 calc(var(--color-hue) + 108)); - --color-c3b3: lch(33 35 calc(var(--color-hue) + 108)); - --color-c3b4: lch(44 30 calc(var(--color-hue) + 108)); - --color-c3b5: lch(55 25 calc(var(--color-hue) + 108)); - --color-c3b6: lch(66 20 calc(var(--color-hue) + 108)); - --color-c3b7: lch(77 15 calc(var(--color-hue) + 108)); - --color-c3b8: lch(88 10 calc(var(--color-hue) + 108)); - --color-c3b9: lch(99 5 calc(var(--color-hue) + 108)); - --color-c4b0: lch(0 50 calc(var(--color-hue) + 144)); - --color-c4b1: lch(11 45 calc(var(--color-hue) + 144)); - --color-c4b2: lch(22 40 calc(var(--color-hue) + 144)); - --color-c4b3: lch(33 35 calc(var(--color-hue) + 144)); - --color-c4b4: lch(44 30 calc(var(--color-hue) + 144)); - --color-c4b5: lch(55 25 calc(var(--color-hue) + 144)); - --color-c4b6: lch(66 20 calc(var(--color-hue) + 144)); - --color-c4b7: lch(77 15 calc(var(--color-hue) + 144)); - --color-c4b8: lch(88 10 calc(var(--color-hue) + 144)); - --color-c4b9: lch(99 5 calc(var(--color-hue) + 144)); - --color-c5b0: lch(0 50 calc(var(--color-hue) + 180)); - --color-c5b1: lch(11 45 calc(var(--color-hue) + 180)); - --color-c5b2: lch(22 40 calc(var(--color-hue) + 180)); - --color-c5b3: lch(33 35 calc(var(--color-hue) + 180)); - --color-c5b4: lch(44 30 calc(var(--color-hue) + 180)); - --color-c5b5: lch(55 25 calc(var(--color-hue) + 180)); - --color-c5b6: lch(66 20 calc(var(--color-hue) + 180)); - --color-c5b7: lch(77 15 calc(var(--color-hue) + 180)); - --color-c5b8: lch(88 10 calc(var(--color-hue) + 180)); - --color-c5b9: lch(99 5 calc(var(--color-hue) + 180)); - --color-c6b0: lch(0 50 calc(var(--color-hue) + 216)); - --color-c6b1: lch(11 45 calc(var(--color-hue) + 216)); - --color-c6b2: lch(22 40 calc(var(--color-hue) + 216)); - --color-c6b3: lch(33 35 calc(var(--color-hue) + 216)); - --color-c6b4: lch(44 30 calc(var(--color-hue) + 216)); - --color-c6b5: lch(55 25 calc(var(--color-hue) + 216)); - --color-c6b6: lch(66 20 calc(var(--color-hue) + 216)); - --color-c6b7: lch(77 15 calc(var(--color-hue) + 216)); - --color-c6b8: lch(88 10 calc(var(--color-hue) + 216)); - --color-c6b9: lch(99 5 calc(var(--color-hue) + 216)); - --color-c7b0: lch(0 50 calc(var(--color-hue) + 252)); - --color-c7b1: lch(11 45 calc(var(--color-hue) + 252)); - --color-c7b2: lch(22 40 calc(var(--color-hue) + 252)); - --color-c7b3: lch(33 35 calc(var(--color-hue) + 252)); - --color-c7b4: lch(44 30 calc(var(--color-hue) + 252)); - --color-c7b5: lch(55 25 calc(var(--color-hue) + 252)); - --color-c7b6: lch(66 20 calc(var(--color-hue) + 252)); - --color-c7b7: lch(77 15 calc(var(--color-hue) + 252)); - --color-c7b8: lch(88 10 calc(var(--color-hue) + 252)); - --color-c7b9: lch(99 5 calc(var(--color-hue) + 252)); - --color-c8b0: lch(0 50 calc(var(--color-hue) + 288)); - --color-c8b1: lch(11 45 calc(var(--color-hue) + 288)); - --color-c8b2: lch(22 40 calc(var(--color-hue) + 288)); - --color-c8b3: lch(33 35 calc(var(--color-hue) + 288)); - --color-c8b4: lch(44 30 calc(var(--color-hue) + 288)); - --color-c8b5: lch(55 25 calc(var(--color-hue) + 288)); - --color-c8b6: lch(66 20 calc(var(--color-hue) + 288)); - --color-c8b7: lch(77 15 calc(var(--color-hue) + 288)); - --color-c8b8: lch(88 10 calc(var(--color-hue) + 288)); - --color-c8b9: lch(99 5 calc(var(--color-hue) + 288)); - --color-c9b0: lch(0 50 calc(var(--color-hue) + 324)); - --color-c9b1: lch(11 45 calc(var(--color-hue) + 324)); - --color-c9b2: lch(22 40 calc(var(--color-hue) + 324)); - --color-c9b3: lch(33 35 calc(var(--color-hue) + 324)); - --color-c9b4: lch(44 30 calc(var(--color-hue) + 324)); - --color-c9b5: lch(55 25 calc(var(--color-hue) + 324)); - --color-c9b6: lch(66 20 calc(var(--color-hue) + 324)); - --color-c9b7: lch(77 15 calc(var(--color-hue) + 324)); - --color-c9b8: lch(88 10 calc(var(--color-hue) + 324)); - --color-c9b9: lch(99 5 calc(var(--color-hue) + 324)); -} diff --git a/src/colors.mjs b/src/colors.mjs new file mode 100644 index 0000000..7975039 --- /dev/null +++ b/src/colors.mjs @@ -0,0 +1,93 @@ +import Color from "colorjs.io"; + +/** + * Contains the color codes with zero (0) being the base hue. + * + * @type {number[]} + */ +export const colorList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + +/** + * Contains the standard brightness codes with zero (0) being the darkest and + * nine (9) being the lightest. + * + * @type {number[]} + */ +export const brightnessList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + +/** + * Gets the degree rotation for the given color code. + * + * @param {number} base The base hue, in the range [0-360) + * @param {number} color The color code, in the range of [0-9] + */ +export function getHue(base, color) { + return (base + color * 36) % 360; +} + +/** + * Gets the lighteness for a given brightness. + * + * @param {number} brightness The brightenss, in the range of [0-9] + */ +export function getLightness(brightness) { + return 5 + brightness * 10; +} + +/** + * Gets the saturation for a given brightness. + * + * @param {number} brightness The brightenss, in the range of [0-9] + */ +export function getChroma(brightness) { + return 50 - brightness * 5; +} + +/** + * Get the LCH string + * + * @param {number} base The base hue, in the range [0-360) + * @param {number} color The color code, in the range of [0-9] + * @param {number} brightness The brightenss, in the range of [0-9] + */ +export function getLchCss(base, color, brightness) { + const l = getLightness(brightness); + const c = getChroma(brightness); + const h = getHue(base, color); + + return `lch(${l}% ${c} ${h})`; +} + +/** + * Gets the RGB values as a string hex of a hash plus 6 numbers. + * + * @param {number} base The base hue, in the range [0-360) + * @param {number} color The color code, in the range of [0-9] + * @param {number} brightness The brightenss, in the range of [0-9] + */ +export function getRgbHex(base, color, brightness) { + const lch = getLchCss(base, color, brightness); + const lchColor = new Color(lch); + const hex = lchColor + .to("srgb") + .toString({ format: "hex", inGamut: true }) + .replace(/^#(\d)(\d)(\d)$/, "#$1$1$2$2$3$3"); + + return hex; +} + +/** + * Gets the RGB values as an array of three numbers. + * + * @param {number} base The base hue, in the range [0-360) + * @param {number} color The color code, in the range of [0-9] + * @param {number} brightness The brightenss, in the range of [0-9] + */ +export function getRgbArray(base, color, brightness) { + const hex = getRgbHex(base, color, brightness); + const r = parseInt(hex.substring(1, 3), "16"); + const g = parseInt(hex.substring(3, 5), "16"); + const b = parseInt(hex.substring(5, 7), "16"); + + return [r, g, b]; +}