171 lines
4.2 KiB
TypeScript
171 lines
4.2 KiB
TypeScript
// Adapted from @k-vyn/coloralgorithm
|
|
|
|
import bezier from "bezier-easing";
|
|
import chroma, { Scale } from "chroma-js";
|
|
import { Curve } from "./curves";
|
|
import { ColorFamily, ColorProps, ColorSet } from "./types";
|
|
|
|
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();
|
|
|
|
// Roughly calculate if a color is dark or light
|
|
const isLight = lch[0] > 50;
|
|
|
|
const result = {
|
|
step,
|
|
hex,
|
|
lch,
|
|
rgbaArray,
|
|
isLight,
|
|
};
|
|
|
|
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 = typeof start === "string" ? validColor(start) : start;
|
|
const middleColor = typeof middle === "string" ? validColor(middle) : middle;
|
|
const endColor = typeof end === "string" ? validColor(end) : 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;
|
|
}
|
|
|
|
export function generateColorsUsingCurve(
|
|
startColor: string,
|
|
endColor: string,
|
|
curve: number[]
|
|
) {
|
|
const NUM_STEPS = 101;
|
|
|
|
const easing = bezier(curve[0], curve[1], curve[2], curve[3]);
|
|
const curveProgress = [];
|
|
for (let i = 0; i <= NUM_STEPS; i++) {
|
|
curveProgress.push(easing(i / NUM_STEPS));
|
|
}
|
|
|
|
const colors: chroma.Color[] = [];
|
|
for (let i = 0; i < NUM_STEPS; i++) {
|
|
// Use HSL as an input as it is easier to construct programatically
|
|
// const color = chroma.hsl();
|
|
const color = chroma.mix(startColor, endColor, curveProgress[i], "lch");
|
|
colors.push(color);
|
|
}
|
|
|
|
return colors;
|
|
}
|
|
|
|
export function generateColors2(
|
|
hue: {
|
|
start: number;
|
|
end: number;
|
|
curve: Curve;
|
|
},
|
|
saturation: {
|
|
start: number;
|
|
end: number;
|
|
curve: Curve;
|
|
},
|
|
lightness: {
|
|
start: number;
|
|
end: number;
|
|
curve: Curve;
|
|
}
|
|
) {
|
|
const NUM_STEPS = 9;
|
|
|
|
const hueEasing = bezier(
|
|
hue.curve.value[0],
|
|
hue.curve.value[1],
|
|
hue.curve.value[2],
|
|
hue.curve.value[3]
|
|
);
|
|
const saturationEasing = bezier(
|
|
saturation.curve.value[0],
|
|
saturation.curve.value[1],
|
|
saturation.curve.value[2],
|
|
saturation.curve.value[3]
|
|
);
|
|
const lightnessEasing = bezier(
|
|
lightness.curve.value[0],
|
|
lightness.curve.value[1],
|
|
lightness.curve.value[2],
|
|
lightness.curve.value[3]
|
|
);
|
|
|
|
const colors: chroma.Color[] = [];
|
|
for (let i = 0; i < NUM_STEPS; i++) {
|
|
const hueValue =
|
|
hueEasing(i / NUM_STEPS) * (hue.end - hue.start) + hue.start;
|
|
const saturationValue =
|
|
saturationEasing(i / NUM_STEPS) * (saturation.end - saturation.start) +
|
|
saturation.start;
|
|
const lightnessValue =
|
|
lightnessEasing(i / NUM_STEPS) * (lightness.end - lightness.start) +
|
|
lightness.start;
|
|
|
|
const color = chroma.hsl(
|
|
hueValue,
|
|
saturationValue / 100,
|
|
lightnessValue / 100
|
|
);
|
|
colors.push(color);
|
|
}
|
|
|
|
const scale = chroma.scale(colors).mode("lch");
|
|
return scale;
|
|
}
|
|
|
|
/** 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;
|
|
}
|