this.props.onChange(color as any)}
+ key={color}
+ style={{ ...inlineStyles.swatchCircle, backgroundColor: color }}
+ />
+ );
+ });
+ };
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+ Hex
+
+
+ {/* // todo Will add more input methods */}
+
+ {this.props.colors?.length && (
+
+ {this.displayColorSwatches(this.props.colors!)}
+
+ )}
+
+ );
+ }
+}
+
+// @ts-ignore
+export default CustomPicker(CustomColorPicker);
diff --git a/src/Features/colors/RenderShades.tsx b/src/Features/colors/RenderShades.tsx
new file mode 100644
index 0000000..0211023
--- /dev/null
+++ b/src/Features/colors/RenderShades.tsx
@@ -0,0 +1,54 @@
+import { Text } from "@mantine/core";
+
+import { canBeWhite } from "./contrast";
+
+import classes from "./styles.module.css";
+
+export const RenderShades = ({
+ colors,
+ setColor,
+ label,
+}: {
+ colors: string[];
+ setColor: (color: string) => void;
+ label: string;
+}) => (
+
+
+ {label}
+
+
+ {colors.map((color, i) => (
+
{
+ setColor(color);
+ }}
+ className={classes.shades__box}
+ style={{
+ backgroundColor: color,
+ color: canBeWhite(color) ? "white" : "black",
+ }}
+ >
+ {color.toUpperCase()}
+
+ ))}
+
+
+);
diff --git a/src/Features/colors/blindness.ts b/src/Features/colors/blindness.ts
new file mode 100644
index 0000000..0c5433b
--- /dev/null
+++ b/src/Features/colors/blindness.ts
@@ -0,0 +1,158 @@
+import { Convert } from "./utilities";
+
+const hexToRgb = (hex: string) => new Convert().hex2rgb(hex);
+const rgbToHex = (r: number, g: number, b: number) =>
+ new Convert().rgb2hex(r, g, b);
+
+export const simulateColorBlindness = (
+ color: string,
+ blindnessType: string
+) => {
+ const [r, g, b] = hexToRgb(color) ?? [0, 0, 0];
+
+ let perceivedColor = [r, g, b];
+
+ if (blindnessType === "Protanopia") {
+ perceivedColor = [
+ 0.567 * r + 0.433 * g + 0.0 * b,
+ 0.558 * r + 0.442 * g + 0.0 * b,
+ 0.242 * r - 0.1055 * g + 1.053 * b,
+ ];
+ }
+
+ if (blindnessType === "Deuteranopia") {
+ perceivedColor = [
+ 0.625 * r + 0.375 * g + 0.0 * b,
+ 0.7 * r + 0.3 * g + 0.0 * b,
+ 0.15 * r + 0.1 * g + 0.75 * b,
+ ];
+ }
+
+ if (blindnessType === "Tritanopia") {
+ perceivedColor = [
+ 0.95 * r + 0.05 * g + 0.0 * b,
+ 0.433 * r + 0.567 * g + 0.0 * b,
+ 0.475 * r + 0.475 * g + 0.05 * b,
+ ];
+ }
+
+ if (blindnessType === "Protanomaly") {
+ perceivedColor = [
+ 0.817 * r + 0.183 * g + 0.0 * b,
+ 0.333 * r + 0.667 * g + 0.0 * b,
+ 0.0 * r + 0.125 * g + 0.875 * b,
+ ];
+ }
+
+ if (blindnessType === "Deuteranomaly") {
+ perceivedColor = [
+ 0.8 * r + 0.2 * g + 0.0 * b,
+ 0.258 * r + 0.742 * g + 0.0 * b,
+ 0.0 * r + 0.142 * g + 0.858 * b,
+ ];
+ }
+
+ if (blindnessType === "Tritanomaly") {
+ perceivedColor = [
+ 0.967 * r + 0.033 * g + 0.0 * b,
+ 0.0 * r + 0.733 * g + 0.267 * b,
+ 0.0 * r + 0.183 * g + 0.817 * b,
+ ];
+ }
+
+ if (blindnessType === "Achromatopsia") {
+ perceivedColor = [
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ ];
+ }
+
+ if (blindnessType === "Achromatomaly") {
+ perceivedColor = [
+ 0.618 * r + 0.32 * g + 0.062 * b,
+ 0.163 * r + 0.775 * g + 0.062 * b,
+ 0.163 * r + 0.32 * g + 0.516 * b,
+ ];
+ }
+
+ if (blindnessType === "Achromatopsia") {
+ perceivedColor = [
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ 0.299 * r + 0.587 * g + 0.114 * b,
+ ];
+ }
+
+ const simulation = rgbToHex(
+ Math.round(perceivedColor[0]),
+ Math.round(perceivedColor[1]),
+ Math.round(perceivedColor[2])
+ );
+
+ return {
+ simulation,
+ percentage: colorSimilarityPercentage(color, simulation),
+ };
+};
+
+const colorSimilarityPercentage = (color1: string, color2: string): number => {
+ const [r1, g1, b1] = hexToRgb(color1) ?? [0, 0, 0];
+ const [r2, g2, b2] = hexToRgb(color2) ?? [0, 0, 0];
+
+ // Compute Euclidean distance
+ const distance = Math.sqrt(
+ Math.pow(r2 - r1, 2) + Math.pow(g2 - g1, 2) + Math.pow(b2 - b1, 2)
+ );
+
+ // Max possible distance in RGB space is sqrt(3 * 255^2)
+ const maxDistance = Math.sqrt(3 * Math.pow(255, 2));
+
+ // Calculate similarity as a percentage
+ const similarity = 1 - distance / maxDistance;
+
+ return similarity * 100; // Returns similarity percentage
+};
+
+export const blindnessStats = [
+ {
+ name: "Deuteranomaly",
+ description: "5.0% of men, 0.35% of women",
+ info: "Green-weak type of color blindness. Greens are more muted, and reds may be confused with greens.",
+ },
+ {
+ name: "Protanopia",
+ description: "1.3% of men, 0.02% of women",
+ info: "Red-green color blindness, with a leaning towards difficulties distinguishing red hues.",
+ },
+ {
+ name: "Protanomaly",
+ description: "1.3% of men, 0.02% of women",
+ info: "Reduced sensitivity to red light causing reds to be less bright.",
+ },
+ {
+ name: "Deuteranopia",
+ description: "1.2% of men, 0.01% of women",
+ info: "Red-green color blindness, with a leaning towards difficulties distinguishing green hues.",
+ },
+ {
+ name: "Tritanopia",
+ description: "0.001% of men, 0.03% of women",
+ info: "Blue-yellow color blindness. Blues appear greener and it can be hard to tell yellow and red from pink.",
+ },
+ {
+ name: "Tritanomaly",
+ description: "0.0001% of men, 0.0001% of women",
+ info: "Reduced sensitivity to blue light causing blues to be less bright, and difficulties distinguishing between yellow and red from pink.",
+ },
+ {
+ name: "Achromatomaly",
+ description: "0.003% of the population",
+ info: "Reduced sensitivity to light causing colors to appear less bright and less saturated.",
+ },
+ {
+ name: "Achromatopsia",
+ description: "0.0001% of the population",
+ info: "Total color blindness. Colors are seen as completely neutral.",
+ },
+];
diff --git a/src/Features/colors/contrast.ts b/src/Features/colors/contrast.ts
new file mode 100644
index 0000000..78aaa3c
--- /dev/null
+++ b/src/Features/colors/contrast.ts
@@ -0,0 +1,89 @@
+const canBeWhite = (hex: string) => {
+ const ratio = checkContrast(hex, "#ffffff");
+ return ratio >= 4.5;
+};
+
+function luminance(r: number, g: number, b: number) {
+ let [lumR, lumG, lumB] = [r, g, b].map((component) => {
+ let proportion = component / 255;
+
+ return proportion <= 0.03928
+ ? proportion / 12.92
+ : Math.pow((proportion + 0.055) / 1.055, 2.4);
+ });
+
+ return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB;
+}
+
+function contrastRatio(luminance1: number, luminance2: number) {
+ let lighterLum = Math.max(luminance1, luminance2);
+ let darkerLum = Math.min(luminance1, luminance2);
+
+ return (lighterLum + 0.05) / (darkerLum + 0.05);
+}
+
+/**
+ * Because color inputs format their values as hex strings (ex.
+ * #000000), we have to do a little parsing to extract the red,
+ * green, and blue components as numbers before calculating the
+ * luminance values and contrast ratio.
+ */
+function checkContrast(color1: any, color2: any) {
+ let [luminance1, luminance2] = [color1, color2].map((color) => {
+ /* Remove the leading hash sign if it exists */
+ color = color.startsWith("#") ? color.slice(1) : color;
+
+ let r = parseInt(color.slice(0, 2), 16);
+ let g = parseInt(color.slice(2, 4), 16);
+ let b = parseInt(color.slice(4, 6), 16);
+
+ return luminance(r, g, b);
+ });
+
+ return contrastRatio(luminance1, luminance2);
+}
+
+/**
+ * A utility to format ratios as nice, human-readable strings with
+ * up to two digits after the decimal point (ex. "4.3:1" or "17:1")
+ */
+function formatRatio(ratio: number) {
+ let ratioAsFloat = ratio.toFixed(2);
+ let isInteger = Number.isInteger(parseFloat(ratioAsFloat));
+ return `${isInteger ? Math.floor(ratio) : ratioAsFloat}:1`;
+}
+
+/**
+ * Determine whether the given contrast ratio meets WCAG
+ * requirements at any level (AA Large, AA, or AAA). In the return
+ * value, `isPass` is true if the ratio meets or exceeds the minimum
+ * of at least one level, and `maxLevel` is the strictest level that
+ * the ratio passes.
+ */
+const WCAG_MINIMUM_RATIOS = [
+ ["AA Large", 3],
+ ["AA", 4.5],
+ ["AAA", 7],
+];
+
+const NameMapping = {
+ "AA Large": "Large text",
+ AA: "Small text",
+ AAA: "Graphics",
+};
+
+function meetsMinimumRequirements(ratio: number) {
+ let didPass = false;
+ let maxLevel = null;
+
+ const checks = WCAG_MINIMUM_RATIOS.map(([level, minRatio]) => {
+ return {
+ level,
+ pass: ratio >= (minRatio as number),
+ label: NameMapping[level as keyof typeof NameMapping],
+ };
+ });
+ return checks;
+}
+
+export { checkContrast, formatRatio, meetsMinimumRequirements, canBeWhite };
diff --git a/src/Features/colors/generator.ts b/src/Features/colors/generator.ts
index 97abc16..2df2a83 100644
--- a/src/Features/colors/generator.ts
+++ b/src/Features/colors/generator.ts
@@ -31,23 +31,3 @@ export function generateColorsMap(color: string) {
return { baseColorIndex, colors };
}
-
-export type MantineColorsTuple = readonly [
- string,
- string,
- string,
- string,
- string,
- string,
- string,
- string,
- string,
- string,
- ...string[],
-];
-
-export function generateColors(color: string) {
- return generateColorsMap(color).colors.map((c) =>
- c.hex()
- ) as unknown as MantineColorsTuple;
-}
diff --git a/src/Features/colors/harmonies.ts b/src/Features/colors/harmonies.ts
new file mode 100644
index 0000000..630282e
--- /dev/null
+++ b/src/Features/colors/harmonies.ts
@@ -0,0 +1,109 @@
+// Harmonies
+
+import { Convert } from "./utilities";
+
+const cc = new Convert();
+
+export function analogous(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const a1 = cc.lch2hex(l, c, (h + 30) % 360);
+ const a2 = cc.lch2hex(l, c, (h + 15) % 360);
+ const a3 = cc.lch2hex(l, c, (h - 15 + 360) % 360);
+ const a4 = cc.lch2hex(l, c, (h - 30 + 360) % 360);
+
+ return [main, a1, a2, a3, a4];
+}
+
+export function monochromatic(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const m1 = cc.lch2hex(l * 0.4, c, h); // Reducing lightness for shades
+ const m2 = cc.lch2hex(l * 0.6, c * 0.75, h); // Reduced chroma and lightness
+ const m3 = cc.lch2hex(l * 0.8, c * 0.5, h); // Further reduction in chroma and lightness
+ const m4 = cc.lch2hex(l * 0.9, c * 0.25, h); // Minimal chroma and near full lightness
+
+ return [main, m1, m2, m3, m4];
+}
+
+export function triadic(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const t1 = cc.lch2hex(l, c, (h + 120) % 360);
+ const t2 = cc.lch2hex(l, c * 0.75, (h + 240) % 360);
+ const t3 = cc.lch2hex(l * 0.6, c * 0.5, (h + 240) % 360);
+ const t4 = cc.lch2hex(l * 0.4, c * 0.25, (h + 240) % 360);
+
+ return [main, t1, t2, t3, t4];
+}
+
+// Complementary Colors
+export function complementary(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const c1 = cc.lch2hex(l, c, (h + 180) % 360);
+ const c2 = cc.lch2hex(l, c * 0.75, (h + 60) % 360);
+ const c3 = cc.lch2hex(l * 0.6, c * 0.5, (h + 60) % 360);
+ const c4 = cc.lch2hex(l * 0.4, c * 0.25, (h + 60) % 360);
+
+ return [main, c1, c2, c3, c4];
+}
+
+export function directComplementary(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+ return cc.lch2hex(l, c, (h + 180) % 360);
+}
+// Split Complementary Colors
+export function splitComplementary(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const sc1 = cc.lch2hex(l, c, (h + 150) % 360);
+ const sc2 = cc.lch2hex(l, c, (h + 210) % 360);
+ const sc3 = cc.lch2hex(l * 0.6, c * 0.75, (h + 60) % 360);
+ const sc4 = cc.lch2hex(l * 0.4, c * 0.5, (h + 60) % 360);
+
+ return [main, sc1, sc2, sc3, sc4];
+}
+
+export function doubleSplitComplementary(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const dsc1 = cc.lch2hex(l, c * 0.75, (h + 150) % 360);
+ const dsc2 = cc.lch2hex(l, c * 0.75, (h + 210) % 360);
+ const dsc3 = cc.lch2hex(l, c * 0.75, (h + 270) % 360);
+ const dsc4 = cc.lch2hex(l, c * 0.75, (h + 30) % 360);
+
+ return [main, dsc1, dsc2, dsc3, dsc4];
+}
+
+// Square Colors
+export function square(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const s1 = cc.lch2hex(l, c, (h + 90) % 360);
+ const s2 = cc.lch2hex(l, c, (h + 180) % 360);
+ const s3 = cc.lch2hex(l, c, (h + 270) % 360);
+ const s4 = cc.lch2hex(l, c, (h + 45) % 360); // Typically for a square, the hues should be 90 degrees apart, this seems like a special case.
+
+ return [main, s1, s2, s3, s4];
+}
+
+// Compound Colors
+export function compound(hex: string) {
+ const [l, c, h] = cc.hex2lch(hex);
+
+ const main = cc.lch2hex(l, c, h);
+ const cp1 = cc.lch2hex(l, c * 0.75, (h + 30) % 360);
+ const cp2 = cc.lch2hex(l, c, (h + 150) % 360);
+ const cp3 = cc.lch2hex(l, c, (h + 210) % 360);
+ const cp4 = cc.lch2hex(l, c * 0.75, (h + 330) % 360);
+
+ return [main, cp1, cp2, cp3, cp4];
+}
diff --git a/src/Features/colors/hooks.ts b/src/Features/colors/hooks.ts
new file mode 100644
index 0000000..2de64ff
--- /dev/null
+++ b/src/Features/colors/hooks.ts
@@ -0,0 +1,29 @@
+import React, { useCallback, useEffect, useState } from "react";
+import { getRandomColor } from "./utilities";
+
+export const useColorRandomizer = (): [
+ string,
+ React.Dispatch
>,
+] => {
+ const [color, setColor] = useState("#000000");
+
+ const randomize = useCallback(() => {
+ const randomColor = getRandomColor();
+ setColor(randomColor);
+ }, []);
+
+ useEffect(() => {
+ // Randomize the color on load
+ randomize();
+
+ window.addEventListener("keydown", (e) => {
+ if (e.code === "Space") randomize();
+ });
+
+ return () => {
+ window.removeEventListener("keydown", () => {});
+ };
+ }, []);
+
+ return [color, setColor];
+};
diff --git a/src/Features/colors/styles.module.css b/src/Features/colors/styles.module.css
index 129b0c3..8f553e6 100644
--- a/src/Features/colors/styles.module.css
+++ b/src/Features/colors/styles.module.css
@@ -18,3 +18,97 @@
height: 24px;
width: 24px;
}
+/* Color shades */
+.shades__container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-direction: row;
+ min-height: 45px;
+ width: 100%;
+}
+
+.shades__box {
+ font-size: 0.7em;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ cursor: copy;
+}
+.shades__container :first-child {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+}
+.shades__container :last-child {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+}
+.shades__box span {
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+.shades__box:hover span {
+ opacity: 1;
+}
+/* Wcag contrast box */
+
+.wcagBox {
+ font-size: 14px;
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ width: 40%;
+ border: 1px solid var(--mantine-color-gray-8);
+ border-radius: 5px;
+}
+
+
+.box {
+ display: flex;
+ margin-right: 0.5rem;
+ align-items: center;
+ padding: 0.125rem 0.5rem;
+ border: 2px;
+ text-align: center;
+ font-size: 1rem;
+ border-radius: 0.25rem;
+
+ &.dark {
+ background-color: var(--mantine-color-gray-7);
+ border: 1px solid var(--mantine-color-gray-7);
+ }
+ &.light {
+ background-color: var(--mantine-color-gray-3);
+ border: 1px solid var(--mantine-color-gray-3);
+ }
+}
+
+.grid {
+ margin-top: 10px;
+ display: grid;
+ gap: 0.5em;
+ grid-template-columns: repeat(2, 1fr);
+}
+
+.ok {
+ color: #0ca678;
+}
+
+.fail {
+ color: #ff5252;
+}
+
+.colorPicker {
+ cursor: pointer !important;
+}
+
+
+/* Simulation */
+.simulation_heading {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
diff --git a/src/Features/colors/utilities.ts b/src/Features/colors/utilities.ts
new file mode 100644
index 0000000..bc2bf65
--- /dev/null
+++ b/src/Features/colors/utilities.ts
@@ -0,0 +1,389 @@
+export const {
+ abs,
+ atan2,
+ cbrt,
+ cos,
+ exp,
+ floor,
+ max,
+ min,
+ PI,
+ pow,
+ sin,
+ sqrt,
+} = Math;
+
+export const epsilon = pow(6, 3) / pow(29, 3);
+export const kappa = pow(29, 3) / pow(3, 3);
+export const precision = 100000000;
+export const [wd50X, wd50Y, wd50Z] = [96.42, 100, 82.49];
+
+// Degree and Radian conversion utilities
+export const deg2rad = (degrees: number) => (degrees * PI) / 180;
+export const rad2deg = (radians: number) => (radians * 180) / PI;
+export const atan2d = (y: number, x: number) => rad2deg(atan2(y, x));
+export const cosd = (degrees: number) => cos(deg2rad(degrees));
+export const sind = (degrees: number) => sin(deg2rad(degrees));
+
+const matrix = (params: number[], mats: any[]) => {
+ return mats.map((mat: any[]) =>
+ mat.reduce(
+ (acc: number, value: number, index: string | number) =>
+ acc +
+ (params[index as number] * precision * (value * precision)) /
+ precision /
+ precision,
+ 0
+ )
+ );
+};
+
+const hexColorMatch =
+ /^#?(?:([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?|([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?)$/i;
+const fixFloat = (num: number) => parseFloat((num * 100).toFixed(1));
+
+export class Convert {
+ hex2rgb = (hex: string) => {
+ // #{3,4,6,8}
+ const [, r, g, b, a, rr, gg, bb, aa] = hex.match(hexColorMatch) || [];
+ if (rr !== undefined || r !== undefined) {
+ const red = rr !== undefined ? parseInt(rr, 16) : parseInt(r + r, 16);
+ const green = gg !== undefined ? parseInt(gg, 16) : parseInt(g + g, 16);
+ const blue = bb !== undefined ? parseInt(bb, 16) : parseInt(b + b, 16);
+ const alpha =
+ aa !== undefined
+ ? parseInt(aa, 16)
+ : a !== undefined
+ ? parseInt(a + a, 16)
+ : 255;
+ return [red, green, blue, alpha].map((c) => (c * 100) / 255);
+ }
+ return [0, 0, 0, 1];
+ // hex = hex.startsWith("#") ? hex.slice(1) : hex;
+ // if (hex.length === 3) {
+ // hex = Array.from(hex).reduce((str, x) => str + x + x, ""); // 123 -> 112233
+ // }
+ // return hex
+ // .split(/([a-z0-9]{2,2})/)
+ // .filter(Boolean)
+ // .map((x) => parseInt(x, 16));
+ // return `rgb${values.length == 4 ? "a" : ""}(${values.join(", ")})`;
+ };
+
+ hex2hsv = (hex: string) => {
+ const [h, s, l] = this.hex2hsl(hex) as [number, number, number];
+ return this.hsl2hsv(h, s, l);
+ };
+
+ hex2hsl = (hex: string, asObj = false) => {
+ const [r, g, b] = this.hex2rgb(hex);
+ const max = Math.max(r, g, b),
+ min = Math.min(r, g, b);
+ let h: number,
+ s: number,
+ l = (max + min) / 2;
+
+ if (max === min) {
+ h = s = 0; // achromatic
+ } else {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ h =
+ max === r
+ ? (g - b) / d + (g < b ? 6 : 0)
+ : max === g
+ ? (b - r) / d + 2
+ : (r - g) / d + 4;
+ h *= 60;
+ if (h < 0) h += 360;
+ }
+
+ return [Math.round(h), fixFloat(s), fixFloat(l)];
+ };
+
+ hsl2Object = (hsl: number[]) => {
+ const [h, s, l] = hsl;
+ return { h, s, l };
+ };
+
+ hex2lch = (hex: string) => {
+ const rgb = this.hex2rgb(hex);
+ if (!rgb) return [0, 0, 0];
+ return this.rgb2lch(...(rgb as [number, number, number]));
+ };
+
+ rgb2hex = (r: number, g: number, b: number) => {
+ return `#${((1 << 24) + (Math.round((r * 255) / 100) << 16) + (Math.round((g * 255) / 100) << 8) + Math.round((b * 255) / 100)).toString(16).slice(1)}`;
+ };
+
+ rgb2lch = (r: number, g: number, b: number) => {
+ const [x, y, z] = this.rgb2xyz(r, g, b);
+ const [_l, _a, _b] = this.xyz2lab(x, y, z);
+ return this.lab2lch(_l, _a, _b);
+ };
+
+ rgb2xyz = (r: number, g: number, b: number) => {
+ const [lR, lB, lG] = [r, g, b].map((v) =>
+ v > 4.045 ? Math.pow((v + 5.5) / 105.5, 2.4) * 100 : v / 12.92
+ );
+ return matrix(
+ [lR, lB, lG],
+ [
+ [0.4124564, 0.3575761, 0.1804375],
+ [0.2126729, 0.7151522, 0.072175],
+ [0.0193339, 0.119192, 0.9503041],
+ ]
+ );
+ };
+
+ hsv2hsl = (h: number, s: number, v: number) => {
+ const l = ((200 - s) * v) / 200;
+ s = l === 0 || l === 100 ? 0 : (s * v) / 100 / (l < 50 ? l : 100 - l);
+ return [h, s * 100, l / 2];
+ };
+
+ hsl2hsv = (h: number, s: number, l: number) => {
+ const v = l + (s * Math.min(l, 100 - l)) / 100;
+ s = v === 0 ? 0 : 2 * (1 - l / v);
+ return { h, s: s * 100, v };
+ };
+
+ hslToRgb = (h: number, s: number, l: number) => {
+ let r, g, b;
+ if (s == 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const hue2rgb = function hue2rgb(p: number, q: number, t: number) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ };
+ let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ let p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1 / 3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1 / 3);
+ }
+ return [
+ Math.min(255, Math.max(0, Math.round(r * 255))),
+ Math.min(255, Math.max(0, Math.round(g * 255))),
+ Math.min(255, Math.max(0, Math.round(b * 255))),
+ ];
+ };
+
+ hsv2rgb = (h: number, s: number, v: number, a: number) => {
+ const rgbI = floor(h / 60);
+ // calculate rgb parts
+ const rgbF = (h / 60 - rgbI) & 1 ? h / 60 - rgbI : 1 - h / 60 - rgbI;
+ const rgbM = (v * (100 - s)) / 100;
+ const rgbN = (v * (100 - s * rgbF)) / 100;
+ const rgbA = a / 100;
+
+ const [rgbR, rgbG, rgbB] =
+ rgbI === 5
+ ? [v, rgbM, rgbN]
+ : rgbI === 4
+ ? [rgbN, rgbM, v]
+ : rgbI === 3
+ ? [rgbM, rgbN, v]
+ : rgbI === 2
+ ? [rgbM, v, rgbN]
+ : rgbI === 1
+ ? [rgbN, v, rgbM]
+ : [v, rgbN, rgbM];
+ return [rgbR, rgbG, rgbB, rgbA];
+ };
+
+ lch2hex = (l: number, c: number, h: number) => {
+ const [r, g, b] = this.lch2rgb(l, c, h);
+ return this.rgb2hex(r, g, b);
+ };
+
+ lch2rgb = (lchL: number, lchC: number, lchH: number) => {
+ const [labL, labA, labB] = this.lch2lab(lchL, lchC, lchH);
+ const [xyzX, xyzY, xyzZ] = this.lab2xyz(labL, labA, labB);
+ const [rgbR, rgbG, rgbB] = this.xyz2rgb(xyzX, xyzY, xyzZ);
+ return [rgbR, rgbG, rgbB];
+ };
+
+ lch2lab = (lchL: number, lchC: number, lchH: number) => {
+ // convert to Lab a and b from the polar form
+ const [labA, labB] = [lchC * cosd(lchH), lchC * sind(lchH)];
+ return [lchL, labA, labB];
+ };
+
+ xyz2rgb = (xyzX: number, xyzY: number, xyzZ: number) => {
+ const [lrgbR, lrgbB, lrgbG] = matrix(
+ [xyzX, xyzY, xyzZ],
+ [
+ [3.2404542, -1.5371385, -0.4985314],
+ [-0.969266, 1.8760108, 0.041556],
+ [0.0556434, -0.2040259, 1.0572252],
+ ]
+ );
+ const [rgbR, rgbG, rgbB] = [lrgbR, lrgbB, lrgbG].map((v) =>
+ v > 0.31308 ? 1.055 * pow(v / 100, 1 / 2.4) * 100 - 5.5 : 12.92 * v
+ );
+ return [rgbR, rgbG, rgbB];
+ };
+
+ xyz2lab = (x: number, y: number, z: number) => {
+ // calculate D50 XYZ from D65 XYZ
+ const [d50X, d50Y, d50Z] = matrix(
+ [x, y, z],
+ [
+ [1.0478112, 0.0228866, -0.050127],
+ [0.0295424, 0.9904844, -0.0170491],
+ [-0.0092345, 0.0150436, 0.7521316],
+ ]
+ );
+ // calculate f
+ const [f1, f2, f3] = [d50X / wd50X, d50Y / wd50Y, d50Z / wd50Z].map(
+ (value) => (value > epsilon ? cbrt(value) : (kappa * value + 16) / 116)
+ );
+ return [116 * f2 - 16, 500 * (f1 - f2), 200 * (f2 - f3)];
+ };
+
+ lab2xyz = (labL: number, labA: number, labB: number) => {
+ // compute f, starting with the luminance-related term
+ const f2 = (labL + 16) / 116;
+ const f1 = labA / 500 + f2;
+ const f3 = f2 - labB / 200;
+ // compute pre-scaled XYZ
+ const [initX, initY, initZ] = [
+ pow(f1, 3) > epsilon ? pow(f1, 3) : (116 * f1 - 16) / kappa,
+ labL > kappa * epsilon ? pow((labL + 16) / 116, 3) : labL / kappa,
+ pow(f3, 3) > epsilon ? pow(f3, 3) : (116 * f3 - 16) / kappa,
+ ];
+ const [xyzX, xyzY, xyzZ] = matrix(
+ // compute XYZ by scaling pre-scaled XYZ by reference white
+ [initX * wd50X, initY * wd50Y, initZ * wd50Z],
+ // calculate D65 XYZ from D50 XYZ
+ [
+ [0.9555766, -0.0230393, 0.0631636],
+ [-0.0282895, 1.0099416, 0.0210077],
+ [0.0122982, -0.020483, 1.3299098],
+ ]
+ );
+ return [xyzX, xyzY, xyzZ];
+ };
+
+ lab2lch = (labL: number, labA: number, labB: number) => {
+ return [
+ labL,
+ sqrt(pow(labA, 2) + pow(labB, 2)), // convert to chroma
+ rad2deg(atan2(labB, labA)), // convert to hue, in degrees
+ ];
+ };
+
+ canBeWhite = (hex: string) => {
+ const [_h, _s, l] = this.hex2hsl(hex);
+ return l < 55;
+ };
+}
+
+const c = new Convert();
+
+export const renderHsl = (hsl: number[]) =>
+ `${hsl[0].toFixed()}, ${(hsl[1] * -1).toFixed()}%, ${(hsl[2] / 100).toFixed()}%`;
+
+export const hex2cmyk = (hex: string) => {
+ return ChromaJS(hex).cmyk();
+};
+export const renderCmyk = (cmyk: number[]) =>
+ cmyk.map((v) => (v * 100).toFixed()).join(", ");
+
+import ChromaJS from "chroma-js";
+
+const intLch2hex = (l: number, c: number, h: number) =>
+ ChromaJS.lch(l, c, h).hex();
+
+const interpolate = (
+ start: number,
+ end: number,
+ step: number,
+ maxStep: number
+) => {
+ let diff = end - start;
+ if (Math.abs(diff) > 180) {
+ diff = -(Math.sign(diff) * (360 - Math.abs(diff)));
+ }
+ return (start + (diff * step) / maxStep) % 360;
+};
+
+export const interpolateColor = (
+ color: [number, number, number],
+ interpolateBy: "l" | "c" | "h",
+ steps: number,
+ endValue: number
+): string[] => {
+ const [l, c, h] = color;
+ const interpolatedColors: string[] = [];
+
+ for (let i = 0; i < steps; i++) {
+ let currentL = l,
+ currentC = c,
+ currentH = h;
+
+ switch (interpolateBy) {
+ case "l":
+ currentL = interpolate(l, endValue, i, steps - 1);
+ break;
+ case "c":
+ currentC = interpolate(c, endValue, i, steps - 1); // Grey color
+ break;
+ case "h":
+ currentH = interpolate(h, endValue, i, steps - 1);
+ break;
+ }
+
+ interpolatedColors.push(render_lch2hex(currentL, currentC, currentH));
+ }
+
+ return interpolatedColors;
+};
+
+const render_lch2hex = (l: number, c: number, h: number) =>
+ ChromaJS.lch(l, c, h).hex();
+
+export const interpolateTwoColors = (
+ c1: [number, number, number],
+ c2: [number, number, number],
+ steps: number
+) => {
+ let interpolatedColorArray = [];
+
+ for (let i = 0; i < steps; i++) {
+ interpolatedColorArray.push(
+ render_lch2hex(
+ interpolate(c1[0], c2[0], i, steps - 1), // interpolate lightness
+ interpolate(c1[1], c2[1], i, steps - 1), // interpolate chroma
+ interpolate(c1[2], c2[2], i, steps - 1) // interpolate hue
+ )
+ );
+ }
+
+ return interpolatedColorArray;
+};
+
+export const getInterpolateShades = (
+ startColor: string,
+ endColor: string,
+ shades: number
+) => {
+ const [l1, c1, h1] = c.hex2lch(startColor) ?? [0, 0, 0];
+ const [l2, c2, h2] = c.hex2lch(endColor) ?? [0, 0, 0];
+
+ return interpolateTwoColors([l1, c1, h1], [l2, c2, h2], shades);
+};
+
+export const getRandomColor = () => {
+ const [r, g, b] = Array.from({ length: 3 }, () =>
+ Math.floor(Math.random() * 256)
+ );
+ const randomColor = new Convert().rgb2hex(r, g, b);
+ return randomColor;
+};
diff --git a/src/Features/ids/Ids.tsx b/src/Features/ids/Ids.tsx
index d61449e..1690d69 100644
--- a/src/Features/ids/Ids.tsx
+++ b/src/Features/ids/Ids.tsx
@@ -8,33 +8,43 @@ import {
Textarea,
} from "@mantine/core";
import { useInputState } from "@mantine/hooks";
-import { useState } from "react";
-import { v1, v3, v4, v5 } from "uuid";
+import { useCallback, useEffect, useState } from "react";
+import { v4 } from "uuid";
+import { nanoid, customAlphabet } from "nanoid";
import { Copy } from "../../Components/Copy";
-type Versions = "v1" | "v3" | "v4" | "v5";
+type Generator = "v4" | "nanoid" | "custom";
export default function Ids() {
const [ids, setIds] = useState([]);
const [count, setCount] = useInputState(5);
- const [version, setVersion] = useInputState("v4");
+ const [generator, setGenerator] = useInputState("v4");
+ const [custom, setCustom] = useInputState<{
+ alphabet: string;
+ length: number;
+ }>({
+ alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+ length: 16,
+ });
- const generateIds = () => {
- switch (version) {
- case "v1":
- // setIds(Array.from({ length: count }, () => v1()));
- break;
- case "v3":
- // setIds(Array.from({ length: count }, () => v3()));
- break;
- case "v4":
- setIds(Array.from({ length: count }, () => v4()));
- break;
- case "v5":
- // setIds(Array.from({ length: count }, () => v5()));
- break;
- }
- };
+ const generateIds = useCallback(() => {
+ const newIds = Array.from({ length: count }, () => {
+ switch (generator) {
+ case "v4":
+ return v4();
+ case "nanoid":
+ return nanoid();
+ case "custom":
+ return customAlphabet(custom.alphabet)(custom.length);
+ }
+ });
+ setIds(newIds);
+ }, [count, generator, custom.length, custom.alphabet]);
+
+ // Set initial IDs
+ useEffect(() => {
+ generateIds();
+ }, [generator]);
return (
@@ -46,14 +56,26 @@ export default function Ids() {
onChange={(e) => setCount(Number(e))}
/>