WIP: Use algorithm to generate reference color palette

Nate Butler created

Adapted from @k-vyn/coloralgorithm
Generate colors for our reference palette.

Change summary

styles/package-lock.json            |  29 +++++
styles/package.json                 |  36 +++---
styles/src/system/algorithm.ts      | 103 +++++++++++++++++++
styles/src/system/lib/curves.ts     | 163 +++++++++++++++++++++++++++++++
styles/src/system/ref/color.ts      | 133 +++++++++++++++++++-----
styles/src/system/reference.ts      |  10 -
styles/theme-tool/app/page.tsx      |  30 ++++
styles/theme-tool/package-lock.json |  29 +++++
styles/theme-tool/package.json      |   1 
9 files changed, 473 insertions(+), 61 deletions(-)

Detailed changes

styles/package-lock.json 🔗

@@ -9,6 +9,7 @@
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {
+                "@k-vyn/coloralgorithm": "^1.0.0",
                 "@types/chroma-js": "^2.1.3",
                 "@types/node": "^17.0.23",
                 "case-anything": "^2.1.10",
@@ -36,6 +37,15 @@
                 "node": ">=12"
             }
         },
+        "node_modules/@k-vyn/coloralgorithm": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/@k-vyn/coloralgorithm/-/coloralgorithm-1.0.0.tgz",
+            "integrity": "sha512-a9aAOXxQ+c2Mw5sMC39elT0wYkPa3qktFjtxVkfY3mQEFBr7NMQEczCARVdkmIKo1dIrgNSx3z12sTXohzSZDg==",
+            "dependencies": {
+                "bezier-easing": "^2.1.0",
+                "chroma-js": "^2.1.0"
+            }
+        },
         "node_modules/@tsconfig/node10": {
             "version": "1.0.8",
             "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
@@ -90,6 +100,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "node_modules/bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "node_modules/case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -212,6 +227,15 @@
                 "@cspotcode/source-map-consumer": "0.8.0"
             }
         },
+        "@k-vyn/coloralgorithm": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/@k-vyn/coloralgorithm/-/coloralgorithm-1.0.0.tgz",
+            "integrity": "sha512-a9aAOXxQ+c2Mw5sMC39elT0wYkPa3qktFjtxVkfY3mQEFBr7NMQEczCARVdkmIKo1dIrgNSx3z12sTXohzSZDg==",
+            "requires": {
+                "bezier-easing": "^2.1.0",
+                "chroma-js": "^2.1.0"
+            }
+        },
         "@tsconfig/node10": {
             "version": "1.0.8",
             "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
@@ -257,6 +281,11 @@
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
+        "bezier-easing": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+            "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+        },
         "case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",

styles/package.json 🔗

@@ -1,20 +1,20 @@
 {
-    "name": "styles",
-    "version": "1.0.0",
-    "description": "",
-    "main": "index.js",
-    "scripts": {
-        "build": "ts-node ./src/buildThemes.ts",
-        "build-licenses": "ts-node ./src/buildLicenses.ts"
-    },
-    "author": "",
-    "license": "ISC",
-    "dependencies": {
-        "@types/chroma-js": "^2.1.3",
-        "@types/node": "^17.0.23",
-        "case-anything": "^2.1.10",
-        "chroma-js": "^2.4.2",
-        "toml": "^3.0.0",
-        "ts-node": "^10.7.0"
-    }
+  "name": "styles",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "build": "ts-node ./src/buildThemes.ts",
+    "build-licenses": "ts-node ./src/buildLicenses.ts"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@types/chroma-js": "^2.1.3",
+    "@types/node": "^17.0.23",
+    "case-anything": "^2.1.10",
+    "chroma-js": "^2.4.2",
+    "toml": "^3.0.0",
+    "ts-node": "^10.7.0"
+  }
 }

styles/src/system/algorithm.ts 🔗

@@ -0,0 +1,103 @@
+// Adapted from @k-vyn/coloralgorithm
+
+import chroma, { Scale } from "chroma-js";
+
+export type Color = {
+  step: number;
+  hex: string;
+  lch: number[];
+  rgbaArray: number[];
+};
+
+export type ColorSet = Color[];
+export type ColorFamily = {
+  name: string;
+  colors: string[];
+  invertedColors: string[];
+  colorsMeta: ColorSet;
+  invertedMeta: ColorSet;
+};
+
+export interface ColorProps {
+  name: string;
+  color: {
+    start: string;
+    middle: string;
+    end: string;
+  };
+}
+
+function validColor(color: string) {
+  if (chroma.valid(color)) {
+    return color;
+  } else {
+    throw new Error(`Invalid color: ${color}`);
+  }
+}
+
+function assignColor(scale: Scale, steps: number, step: number) {
+  const color = scale(step / steps);
+  const lch = color.lch();
+  const rgbaArray = color.rgba();
+  const hex = color.hex();
+
+  const result = {
+    step,
+    hex,
+    lch,
+    rgbaArray,
+  };
+
+  return result;
+}
+
+/** Outputs 101 colors (0-100) */
+export function generateColors(props: ColorProps, inverted: boolean) {
+  const steps = 101;
+  const colors: ColorSet = [];
+
+  const { start, middle, end } = props.color;
+
+  const startColor = validColor(start);
+  const middleColor = validColor(middle);
+  const endColor = validColor(end);
+
+  // TODO: Use curve when generating colors
+
+  let scale: Scale;
+
+  if (inverted) {
+    scale = chroma.scale([endColor, middleColor, startColor]).mode("lch");
+  } else {
+    scale = chroma.scale([startColor, middleColor, endColor]).mode("lch");
+  }
+  for (let i = 0; i < steps; i++) {
+    const color = assignColor(scale, steps, i);
+    colors.push(color);
+  }
+  return colors;
+}
+
+/** Generates two color ramps:
+ * One for for light, and one for dark.
+ * By generating two ramps, rather than two default themes, we can use the same reference palette values for tokens in components.
+ *
+ * Each ramp has 101 colors (0-100)
+ */
+export function generateColorSet(props: ColorProps) {
+  const generatedColors = generateColors(props, false);
+  const generatedInvertedColors = generateColors(props, true);
+
+  const colors = generatedColors.map((color) => color.hex);
+  const invertedColors = generatedInvertedColors.map((color) => color.hex);
+
+  const result: ColorFamily = {
+    name: props.name,
+    colors: colors,
+    invertedColors: invertedColors,
+    colorsMeta: generatedColors,
+    invertedMeta: generatedInvertedColors,
+  };
+
+  return result;
+}

styles/src/system/lib/curves.ts 🔗

@@ -0,0 +1,163 @@
+// Adapted from @k-vyn/coloralgorithm
+
+export interface Curve {
+  name: string;
+  formatted_name: string;
+  value: number[];
+}
+
+export interface Curves {
+  linear: Curve;
+  easeInCubic: Curve;
+  easeOutCubic: Curve;
+  easeInOutCubic: Curve;
+  easeInSine: Curve;
+  easeOutSine: Curve;
+  easeInOutSine: Curve;
+  easeInQuad: Curve;
+  easeOutQuad: Curve;
+  easeInOutQuad: Curve;
+  easeInQuart: Curve;
+  easeOutQuart: Curve;
+  easeInOutQuart: Curve;
+  easeInQuint: Curve;
+  easeOutQuint: Curve;
+  easeInOutQuint: Curve;
+  easeInExpo: Curve;
+  easeOutExpo: Curve;
+  easeInOutExpo: Curve;
+  easeInCirc: Curve;
+  easeOutCirc: Curve;
+  easeInOutCirc: Curve;
+  easeInBack: Curve;
+  easeOutBack: Curve;
+  easeInOutBack: Curve;
+}
+
+export const curve: Curves = {
+  linear: {
+    name: "linear",
+    formatted_name: "Linear",
+    value: [0.5, 0.5, 0.5, 0.5],
+  },
+  easeInCubic: {
+    name: "easeInCubic",
+    formatted_name: "Cubic - EaseIn",
+    value: [0.55, 0.055, 0.675, 0.19],
+  },
+  easeOutCubic: {
+    name: "easeOutCubic",
+    formatted_name: "Cubic - EaseOut",
+    value: [0.215, 0.61, 0.355, 1],
+  },
+  easeInOutCubic: {
+    name: "easeInOutCubic",
+    formatted_name: "Cubic - EaseInOut",
+    value: [0.645, 0.045, 0.355, 1],
+  },
+  easeInSine: {
+    name: "easeInSine",
+    formatted_name: "Sine - EaseIn",
+    value: [0.47, 0, 0.745, 0.715],
+  },
+  easeOutSine: {
+    name: "easeOutSine",
+    formatted_name: "Sine - EaseOut",
+    value: [0.39, 0.575, 0.565, 1],
+  },
+  easeInOutSine: {
+    name: "easeInOutSine",
+    formatted_name: "Sine - EaseInOut",
+    value: [0.445, 0.05, 0.55, 0.95],
+  },
+  easeInQuad: {
+    name: "easeInQuad",
+    formatted_name: "Quad - EaseIn",
+    value: [0.55, 0.085, 0.68, 0.53],
+  },
+  easeOutQuad: {
+    name: "easeOutQuad",
+    formatted_name: "Quad - EaseOut",
+    value: [0.25, 0.46, 0.45, 0.94],
+  },
+  easeInOutQuad: {
+    name: "easeInOutQuad",
+    formatted_name: "Quad - EaseInOut",
+    value: [0.455, 0.03, 0.515, 0.955],
+  },
+  easeInQuart: {
+    name: "easeInQuart",
+    formatted_name: "Quart - EaseIn",
+    value: [0.895, 0.03, 0.685, 0.22],
+  },
+  easeOutQuart: {
+    name: "easeOutQuart",
+    formatted_name: "Quart - EaseOut",
+    value: [0.165, 0.84, 0.44, 1],
+  },
+  easeInOutQuart: {
+    name: "easeInOutQuart",
+    formatted_name: "Quart - EaseInOut",
+    value: [0.77, 0, 0.175, 1],
+  },
+  easeInQuint: {
+    name: "easeInQuint",
+    formatted_name: "Quint - EaseIn",
+    value: [0.755, 0.05, 0.855, 0.06],
+  },
+  easeOutQuint: {
+    name: "easeOutQuint",
+    formatted_name: "Quint - EaseOut",
+    value: [0.23, 1, 0.32, 1],
+  },
+  easeInOutQuint: {
+    name: "easeInOutQuint",
+    formatted_name: "Quint - EaseInOut",
+    value: [0.86, 0, 0.07, 1],
+  },
+  easeInCirc: {
+    name: "easeInCirc",
+    formatted_name: "Circ - EaseIn",
+    value: [0.6, 0.04, 0.98, 0.335],
+  },
+  easeOutCirc: {
+    name: "easeOutCirc",
+    formatted_name: "Circ - EaseOut",
+    value: [0.075, 0.82, 0.165, 1],
+  },
+  easeInOutCirc: {
+    name: "easeInOutCirc",
+    formatted_name: "Circ - EaseInOut",
+    value: [0.785, 0.135, 0.15, 0.86],
+  },
+  easeInExpo: {
+    name: "easeInExpo",
+    formatted_name: "Expo - EaseIn",
+    value: [0.95, 0.05, 0.795, 0.035],
+  },
+  easeOutExpo: {
+    name: "easeOutExpo",
+    formatted_name: "Expo - EaseOut",
+    value: [0.19, 1, 0.22, 1],
+  },
+  easeInOutExpo: {
+    name: "easeInOutExpo",
+    formatted_name: "Expo - EaseInOut",
+    value: [1, 0, 0, 1],
+  },
+  easeInBack: {
+    name: "easeInBack",
+    formatted_name: "Back - EaseIn",
+    value: [0.6, -0.28, 0.735, 0.045],
+  },
+  easeOutBack: {
+    name: "easeOutBack",
+    formatted_name: "Back - EaseOut",
+    value: [0.175, 0.885, 0.32, 1.275],
+  },
+  easeInOutBack: {
+    name: "easeInOutBack",
+    formatted_name: "Back - EaseInOut",
+    value: [0.68, -0.55, 0.265, 1.55],
+  },
+};

styles/src/system/ref/color.ts 🔗

@@ -1,39 +1,110 @@
 import * as chroma from "chroma-js";
+import { ColorFamily, generateColorSet } from "../algorithm";
 
 // Colors should use the LCH color space.
 // https://www.w3.org/TR/css-color-4/#lch-colors
 
-const base = {
-  black: chroma.lch(0, 0, 0),
-  white: chroma.lch(150, 0, 0),
-  gray: {
-    light: chroma.lch(96, 0, 0),
-    mid: chroma.lch(55, 0, 0),
-    dark: chroma.lch(10, 0, 0),
+export const black = chroma.lch(0, 0, 0);
+
+export const white = chroma.lch(150, 0, 0);
+
+// Gray ======================================== //
+
+const gray: ColorFamily = generateColorSet({
+  name: "gray",
+  color: {
+    start: "#F0F0F0",
+    middle: "#787878",
+    end: "#0F0F0F",
+  },
+});
+
+export const grayLight = chroma.scale(gray.colors).mode("lch");
+export const grayDark = chroma.scale(gray.invertedColors).mode("lch");
+
+// Rose ======================================== //
+
+const rose: ColorFamily = generateColorSet({
+  name: "rose",
+  color: {
+    start: "#FFF1F2",
+    middle: "#F43F5E",
+    end: "#881337",
   },
-  rose: {
-    light: chroma.lch(96, 5, 14),
-    mid: chroma.lch(56, 74, 21),
-    dark: chroma.lch(10, 24, 21),
+});
+
+export const roseLight = chroma.scale(rose.colors).mode("lch");
+export const roseDark = chroma.scale(rose.invertedColors).mode("lch");
+
+// Red ======================================== //
+
+const red: ColorFamily = generateColorSet({
+  name: "red",
+  color: {
+    start: "#FEF2F2",
+    middle: "#EF4444",
+    end: "#7F1D1D",
   },
-  red: {
-    light: chroma.lch(96, 4, 31),
-    mid: chroma.lch(55, 77, 31),
-    dark: chroma.lch(10, 24, 31),
+});
+
+export const redLight = chroma.scale(red.colors).mode("lch");
+export const redDark = chroma.scale(red.invertedColors).mode("lch");
+
+// Orange ======================================== //
+
+const orange: ColorFamily = generateColorSet({
+  name: "orange",
+  color: {
+    start: "#FFF7ED",
+    middle: "#F97316",
+    end: "#7C2D12",
   },
-};
-
-export const black = base.black;
-export const white = base.white;
-
-export const gray = chroma.scale([
-  base.gray.light,
-  base.gray.mid,
-  base.gray.dark,
-]);
-export const rose = chroma.scale([
-  base.rose.light,
-  base.rose.mid,
-  base.rose.dark,
-]);
-export const red = chroma.scale([base.red.light, base.red.mid, base.red.dark]);
+});
+
+export const orangeLight = chroma.scale(orange.colors).mode("lch");
+export const orangeDark = chroma.scale(orange.invertedColors).mode("lch");
+
+// Amber ======================================== //
+
+const amber: ColorFamily = generateColorSet({
+  name: "amber",
+  color: {
+    start: "#FFFBEB",
+    middle: "#F59E0B",
+    end: "#78350F",
+  },
+});
+
+export const amberLight = chroma.scale(amber.colors).mode("lch");
+export const amberDark = chroma.scale(amber.invertedColors).mode("lch");
+
+// TODO: Add the rest of the colors.
+// Source: https://www.figma.com/file/YEZ9jsC1uc9o6hgbv4kfxq/Core-color-library?node-id=48%3A816&t=Ae6tY1cVb2fm5xaM-1
+
+// Teal ======================================== //
+
+const teal: ColorFamily = generateColorSet({
+  name: "teal",
+  color: {
+    start: "#E6FFFA",
+    middle: "#14B8A6",
+    end: "#134E4A",
+  },
+});
+
+export const tealLight = chroma.scale(teal.colors).mode("lch");
+export const tealDark = chroma.scale(teal.invertedColors).mode("lch");
+
+const cyan = generateColorSet({
+  name: "cyan",
+  color: {
+    start: "#F0FDFA",
+    middle: "#06BBD4",
+    end: "#164E63",
+  },
+});
+
+export const cyanLight = chroma.scale(cyan.colors).mode("lch");
+export const cyanDark = chroma.scale(cyan.colors).mode("lch");
+
+console.log(JSON.stringify(teal, null, 2));

styles/src/system/reference.ts 🔗

@@ -1,9 +1,3 @@
-import { black, gray, rose, red, white } from "./ref/color";
+import * as color from "./ref/color";
 
-export const color = {
-  white,
-  black,
-  gray,
-  rose,
-  red,
-};
+export { color };

styles/theme-tool/app/page.tsx 🔗

@@ -33,13 +33,35 @@ function ColorChips({ colorScale }: { colorScale: Scale }) {
 }
 
 export default function Home() {
-    const { red, gray, rose } = color;
+    const {
+        grayLight,
+        grayDark,
+        roseDark,
+        roseLight,
+        redDark,
+        redLight,
+        orangeDark,
+        orangeLight,
+        amberDark,
+        amberLight,
+    } = color;
     return (
         <main>
             <div style={{ display: 'flex', gap: '1px' }}>
-                <ColorChips colorScale={gray} />
-                <ColorChips colorScale={rose} />
-                <ColorChips colorScale={red} />
+                <ColorChips colorScale={grayLight} />
+                <ColorChips colorScale={grayDark} />
+
+                <ColorChips colorScale={roseLight} />
+                <ColorChips colorScale={roseDark} />
+
+                <ColorChips colorScale={redLight} />
+                <ColorChips colorScale={redDark} />
+
+                <ColorChips colorScale={orangeLight} />
+                <ColorChips colorScale={orangeDark} />
+
+                <ColorChips colorScale={amberLight} />
+                <ColorChips colorScale={amberDark} />
             </div>
         </main>
     );

styles/theme-tool/package-lock.json 🔗

@@ -9,6 +9,7 @@
       "version": "0.1.0",
       "dependencies": {
         "@ianvs/prettier-plugin-sort-imports": "^3.7.1",
+        "@k-vyn/coloralgorithm": "^1.0.0",
         "@next/font": "13.1.6",
         "@types/chroma-js": "^2.1.5",
         "@types/node": "18.13.0",
@@ -568,6 +569,15 @@
         "@jridgewell/sourcemap-codec": "1.4.14"
       }
     },
+    "node_modules/@k-vyn/coloralgorithm": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@k-vyn/coloralgorithm/-/coloralgorithm-1.0.0.tgz",
+      "integrity": "sha512-a9aAOXxQ+c2Mw5sMC39elT0wYkPa3qktFjtxVkfY3mQEFBr7NMQEczCARVdkmIKo1dIrgNSx3z12sTXohzSZDg==",
+      "dependencies": {
+        "bezier-easing": "^2.1.0",
+        "chroma-js": "^2.1.0"
+      }
+    },
     "node_modules/@next/env": {
       "version": "13.1.6",
       "resolved": "https://registry.npmjs.org/@next/env/-/env-13.1.6.tgz",
@@ -1276,6 +1286,11 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "node_modules/bezier-easing": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+      "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+    },
     "node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -4530,6 +4545,15 @@
         "@jridgewell/sourcemap-codec": "1.4.14"
       }
     },
+    "@k-vyn/coloralgorithm": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@k-vyn/coloralgorithm/-/coloralgorithm-1.0.0.tgz",
+      "integrity": "sha512-a9aAOXxQ+c2Mw5sMC39elT0wYkPa3qktFjtxVkfY3mQEFBr7NMQEczCARVdkmIKo1dIrgNSx3z12sTXohzSZDg==",
+      "requires": {
+        "bezier-easing": "^2.1.0",
+        "chroma-js": "^2.1.0"
+      }
+    },
     "@next/env": {
       "version": "13.1.6",
       "resolved": "https://registry.npmjs.org/@next/env/-/env-13.1.6.tgz",
@@ -4962,6 +4986,11 @@
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
     },
+    "bezier-easing": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
+      "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",

styles/theme-tool/package.json 🔗

@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "@ianvs/prettier-plugin-sort-imports": "^3.7.1",
+    "@k-vyn/coloralgorithm": "^1.0.0",
     "@next/font": "13.1.6",
     "@types/chroma-js": "^2.1.5",
     "@types/node": "18.13.0",