2021-03-31 00:59:45 +00:00
|
|
|
import { Random } from "./random";
|
2021-04-30 11:15:38 +00:00
|
|
|
import { range, clamp } from "./util";
|
2021-04-07 23:39:55 +00:00
|
|
|
import * as colorspaces from "colorspaces";
|
2021-04-30 11:15:38 +00:00
|
|
|
import { ColorTuple, hsluvToHex } from "hsluv";
|
2021-03-31 00:59:45 +00:00
|
|
|
|
2021-04-17 11:33:27 +00:00
|
|
|
type RandomPaletteGenerator = (numEntries: number, rng: Random) => string[];
|
|
|
|
|
2021-04-30 11:15:38 +00:00
|
|
|
export type RandomPaletteAlgorithm =
|
|
|
|
| "RGB"
|
|
|
|
| "CIELUV"
|
|
|
|
| "threevals"
|
|
|
|
| "huecontrast"
|
|
|
|
| "randgrey";
|
2021-04-17 11:33:27 +00:00
|
|
|
|
|
|
|
export const DEFAULT_RANDOM_PALETTE_ALGORITHM: RandomPaletteAlgorithm =
|
2021-04-30 11:15:38 +00:00
|
|
|
"threevals";
|
2021-04-17 11:33:27 +00:00
|
|
|
|
2021-03-31 00:59:45 +00:00
|
|
|
/**
|
|
|
|
* Clamp the given number to be between 0 and 255, then
|
|
|
|
* convert it to hexadecimal.
|
|
|
|
*/
|
|
|
|
export function clampedByteToHex(value: number): string {
|
|
|
|
if (value < 0) {
|
|
|
|
value = 0;
|
|
|
|
} else if (value > 255) {
|
|
|
|
value = 255;
|
|
|
|
}
|
|
|
|
let hex = value.toString(16);
|
|
|
|
if (hex.length === 1) {
|
|
|
|
hex = "0" + hex;
|
|
|
|
}
|
|
|
|
return hex;
|
|
|
|
}
|
|
|
|
|
2021-04-17 11:33:27 +00:00
|
|
|
function createRandomRGBColor(rng: Random): string {
|
|
|
|
const rgb = range(3).map(() => rng.inRange({ min: 0, max: 255, step: 1 }));
|
|
|
|
return "#" + rgb.map(clampedByteToHex).join("");
|
|
|
|
}
|
|
|
|
|
|
|
|
function createRandomCIELUVColor(rng: Random): string {
|
2021-04-07 23:39:55 +00:00
|
|
|
const max_luv_samples = 100;
|
2021-04-30 11:15:38 +00:00
|
|
|
let luvSampleFailed = true;
|
|
|
|
let randColorHex: string = "#000000";
|
2021-04-07 00:33:08 +00:00
|
|
|
|
2021-04-07 23:39:55 +00:00
|
|
|
//See if we can pull out a sample inside the LUV solid
|
|
|
|
for (let i = 0; i < max_luv_samples; i++) {
|
2021-04-11 18:51:27 +00:00
|
|
|
//bounds from https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#cvtcolor
|
2021-04-13 19:41:47 +00:00
|
|
|
let L = rng.inInterval({ min: 0, max: 100 });
|
|
|
|
let u = rng.inInterval({ min: -134, max: 220 });
|
|
|
|
let v = rng.inInterval({ min: -140, max: 122 });
|
2021-04-30 11:15:38 +00:00
|
|
|
let randColor = colorspaces.make_color("CIELUV", [L, u, v]);
|
2021-04-07 23:39:55 +00:00
|
|
|
|
2021-04-30 11:15:38 +00:00
|
|
|
if (randColor.is_displayable() && !(L == 0.0 && (u != 0 || v != 0))) {
|
|
|
|
randColorHex = randColor.as("hex");
|
|
|
|
luvSampleFailed = false;
|
2021-04-07 23:39:55 +00:00
|
|
|
break;
|
2021-04-07 00:33:08 +00:00
|
|
|
}
|
2021-04-07 23:39:55 +00:00
|
|
|
}
|
2021-04-07 00:33:08 +00:00
|
|
|
|
2021-04-07 23:39:55 +00:00
|
|
|
//just sample sRGB if I couldn't sample a random LUV color
|
2021-04-30 11:15:38 +00:00
|
|
|
if (luvSampleFailed) {
|
2021-04-11 18:51:27 +00:00
|
|
|
let rgb = [0, 0, 0].map(
|
|
|
|
() => rng.inRange({ min: 0, max: 255, step: 1 }) / 255.0
|
2021-04-07 23:39:55 +00:00
|
|
|
);
|
2021-04-30 11:15:38 +00:00
|
|
|
let randColor = colorspaces.make_color("sRGB", rgb);
|
|
|
|
randColorHex = randColor.as("hex");
|
2021-04-07 23:39:55 +00:00
|
|
|
}
|
2021-04-07 00:33:08 +00:00
|
|
|
|
2021-04-30 11:15:38 +00:00
|
|
|
return randColorHex;
|
|
|
|
}
|
|
|
|
|
|
|
|
function createRandGrey(rng: Random): string[] {
|
|
|
|
let L1 = rng.inInterval({ min: 0, max: 100 });
|
|
|
|
let L2 = rng.inInterval({ min: 0, max: 100 });
|
|
|
|
let L3 = rng.inInterval({ min: 0, max: 100 });
|
|
|
|
|
|
|
|
let Ls = [L1, L2, L3];
|
|
|
|
|
|
|
|
let h = 0;
|
|
|
|
let Hs = [h, h, h];
|
|
|
|
|
|
|
|
let S = 0;
|
|
|
|
let Ss = [S, S, S];
|
|
|
|
|
|
|
|
//zip
|
|
|
|
let hsls = Ls.map((k, i) => [Hs[i], Ss[i], k]);
|
|
|
|
let hexcolors = hsls.map((x) => hsluvToHex(x as ColorTuple));
|
|
|
|
|
|
|
|
//scramble order
|
|
|
|
hexcolors = rng.uniqueChoices(hexcolors, hexcolors.length);
|
|
|
|
return hexcolors;
|
|
|
|
}
|
|
|
|
|
|
|
|
function create3HColor(rng: Random): string[] {
|
|
|
|
let L = rng.fromGaussian({ mean: 50, stddev: 20 });
|
|
|
|
|
|
|
|
let Ls = [L, L, L];
|
|
|
|
|
|
|
|
Ls = Ls.map((x) => clamp(x, 0, 100));
|
|
|
|
|
|
|
|
let h1 = rng.inInterval({ min: 0, max: 360 }),
|
|
|
|
h2 = 360 * (((h1 + 120) / 360) % 1),
|
|
|
|
h3 = 360 * (((h1 + 240) / 360) % 1);
|
|
|
|
|
|
|
|
let Hs = [h1, h2, h3];
|
|
|
|
|
|
|
|
let S = 100;
|
|
|
|
let Ss = [S, S, S];
|
|
|
|
|
|
|
|
Ss = Ss.map((x) => clamp(x, 0, 100));
|
|
|
|
|
|
|
|
//zip
|
|
|
|
let hsls = Ls.map((k, i) => [Hs[i], Ss[i], k]);
|
|
|
|
let hexcolors = hsls.map((x) => hsluvToHex(x as ColorTuple));
|
|
|
|
|
|
|
|
//scramble order
|
|
|
|
hexcolors = rng.uniqueChoices(hexcolors, hexcolors.length);
|
|
|
|
return hexcolors;
|
|
|
|
}
|
|
|
|
|
|
|
|
function create3VColor(rng: Random): string[] {
|
|
|
|
let lowL_Mean = 20.0,
|
|
|
|
medL_Mean = 40.0,
|
|
|
|
hiL_Mean = 70,
|
|
|
|
lowL_SD = 30.0,
|
|
|
|
medL_SD = lowL_SD,
|
|
|
|
hiL_SD = lowL_SD;
|
|
|
|
|
|
|
|
let Ls = [
|
|
|
|
rng.fromGaussian({ mean: lowL_Mean, stddev: lowL_SD }),
|
|
|
|
rng.fromGaussian({ mean: medL_Mean, stddev: medL_SD }),
|
|
|
|
rng.fromGaussian({ mean: hiL_Mean, stddev: hiL_SD }),
|
|
|
|
];
|
|
|
|
|
|
|
|
Ls = Ls.map((x) => clamp(x, 0, 100));
|
|
|
|
|
|
|
|
//Now we have 3 lightness values, pick a random hue and sat
|
|
|
|
|
|
|
|
let h1 = rng.inInterval({ min: 0, max: 360 }),
|
|
|
|
h2 = 360 * (((h1 + 60 * Number(rng.bool(0.5))) / 360) % 1),
|
|
|
|
h3 = 360 * (((h1 + 180 * Number(rng.bool(0.5))) / 360) % 1);
|
|
|
|
|
|
|
|
let Hs = [h1, h2, h3];
|
|
|
|
|
|
|
|
let Ss = [
|
|
|
|
rng.fromGaussian({ mean: 100, stddev: 40 }),
|
|
|
|
rng.fromGaussian({ mean: 100, stddev: 40 }),
|
|
|
|
rng.fromGaussian({ mean: 100, stddev: 40 }),
|
|
|
|
];
|
|
|
|
Ss = Ss.map((x) => clamp(x, 0, 100));
|
|
|
|
|
|
|
|
//zip
|
|
|
|
let hsls = Ls.map((k, i) => [Hs[i], Ss[i], k]);
|
|
|
|
let hexcolors = hsls.map((x) => hsluvToHex(x as ColorTuple));
|
|
|
|
|
|
|
|
//scramble order
|
|
|
|
hexcolors = rng.uniqueChoices(hexcolors, hexcolors.length);
|
|
|
|
return hexcolors;
|
2021-03-31 00:59:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-04-17 11:33:27 +00:00
|
|
|
* Factory function to take a function that generates a random color
|
|
|
|
* and return a palette generator that just calls it once for every
|
|
|
|
* color in the palette.
|
|
|
|
*/
|
|
|
|
function createSimplePaletteGenerator(
|
|
|
|
createColor: (rng: Random) => string
|
|
|
|
): RandomPaletteGenerator {
|
|
|
|
return (numEntries: number, rng: Random) =>
|
|
|
|
range(numEntries).map(() => createColor(rng));
|
|
|
|
}
|
|
|
|
|
2021-04-30 11:15:38 +00:00
|
|
|
/**
|
|
|
|
* Factory function to make a random palette generator for a triad generator
|
|
|
|
*/
|
|
|
|
|
|
|
|
function createTriadPaletteGenerator(
|
|
|
|
createTriad: (rng: Random) => string[]
|
|
|
|
): RandomPaletteGenerator {
|
|
|
|
return (numEntries: number, rng: Random): string[] => {
|
|
|
|
let colors: string[] = [];
|
|
|
|
let n = Math.floor(numEntries / 3) + 1;
|
|
|
|
|
|
|
|
if (numEntries == 3) {
|
|
|
|
colors = colors.concat(createTriad(rng));
|
|
|
|
} else {
|
|
|
|
for (let i = 0; i < n; i++) colors = colors.concat(createTriad(rng));
|
|
|
|
colors = colors.slice(0, numEntries);
|
|
|
|
}
|
|
|
|
|
|
|
|
return colors;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-04-17 11:33:27 +00:00
|
|
|
const PALETTE_GENERATORS: {
|
|
|
|
[key in RandomPaletteAlgorithm]: RandomPaletteGenerator;
|
|
|
|
} = {
|
|
|
|
RGB: createSimplePaletteGenerator(createRandomRGBColor),
|
|
|
|
CIELUV: createSimplePaletteGenerator(createRandomCIELUVColor),
|
2021-04-30 11:15:38 +00:00
|
|
|
threevals: createTriadPaletteGenerator(create3VColor),
|
|
|
|
huecontrast: createTriadPaletteGenerator(create3HColor),
|
|
|
|
randgrey: createTriadPaletteGenerator(createRandGrey),
|
2021-04-17 11:33:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
export const RANDOM_PALETTE_ALGORITHMS = Object.keys(
|
|
|
|
PALETTE_GENERATORS
|
|
|
|
) as RandomPaletteAlgorithm[];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a random color palette with the given number of entries,
|
|
|
|
* optionally using the given random number generator and the
|
|
|
|
* given algorithm.
|
2021-03-31 00:59:45 +00:00
|
|
|
*
|
|
|
|
* The return value is an Array of strings, where each string is
|
|
|
|
* a color hex hash (e.g. `#ff0000`).
|
|
|
|
*/
|
|
|
|
export function createRandomColorPalette(
|
|
|
|
numEntries: number,
|
2021-04-17 11:33:27 +00:00
|
|
|
rng: Random = new Random(),
|
|
|
|
algorithm: RandomPaletteAlgorithm = DEFAULT_RANDOM_PALETTE_ALGORITHM
|
2021-03-31 00:59:45 +00:00
|
|
|
): string[] {
|
2021-04-17 11:33:27 +00:00
|
|
|
return PALETTE_GENERATORS[algorithm](numEntries, rng);
|
2021-03-31 00:59:45 +00:00
|
|
|
}
|