Baseline color rules working (#111)
Made the new rule the default, but its now easy to play with to adjust the exact color rule we are using. This new one pick 3 values (with a bit of randomness), picks a random hue (then picks 2 more with a small chance of taking the opposite hue), and picks a saturation level that tends to be high.pull/115/head
rodzic
bbc278d061
commit
7003daeebf
|
@ -1,13 +1,19 @@
|
|||
import { Random } from "./random";
|
||||
import { range } from "./util";
|
||||
import { range, clamp } from "./util";
|
||||
import * as colorspaces from "colorspaces";
|
||||
import { ColorTuple, hsluvToHex } from "hsluv";
|
||||
|
||||
type RandomPaletteGenerator = (numEntries: number, rng: Random) => string[];
|
||||
|
||||
export type RandomPaletteAlgorithm = "RGB" | "CIELUV";
|
||||
export type RandomPaletteAlgorithm =
|
||||
| "RGB"
|
||||
| "CIELUV"
|
||||
| "threevals"
|
||||
| "huecontrast"
|
||||
| "randgrey";
|
||||
|
||||
export const DEFAULT_RANDOM_PALETTE_ALGORITHM: RandomPaletteAlgorithm =
|
||||
"CIELUV";
|
||||
"threevals";
|
||||
|
||||
/**
|
||||
* Clamp the given number to be between 0 and 255, then
|
||||
|
@ -33,8 +39,8 @@ function createRandomRGBColor(rng: Random): string {
|
|||
|
||||
function createRandomCIELUVColor(rng: Random): string {
|
||||
const max_luv_samples = 100;
|
||||
let luv_sample_failed = true;
|
||||
let rand_color_hex: string = "#000000";
|
||||
let luvSampleFailed = true;
|
||||
let randColorHex: string = "#000000";
|
||||
|
||||
//See if we can pull out a sample inside the LUV solid
|
||||
for (let i = 0; i < max_luv_samples; i++) {
|
||||
|
@ -42,29 +48,114 @@ function createRandomCIELUVColor(rng: Random): string {
|
|||
let L = rng.inInterval({ min: 0, max: 100 });
|
||||
let u = rng.inInterval({ min: -134, max: 220 });
|
||||
let v = rng.inInterval({ min: -140, max: 122 });
|
||||
let rand_color = colorspaces.make_color("CIELUV", [L, u, v]);
|
||||
let randColor = colorspaces.make_color("CIELUV", [L, u, v]);
|
||||
|
||||
//console.log(`L:${L},u${u},v${v}`);
|
||||
if (rand_color.is_displayable() && !(L == 0.0 && (u != 0 || v != 0))) {
|
||||
rand_color_hex = rand_color.as("hex");
|
||||
//console.log(rand_color_hex);
|
||||
luv_sample_failed = false;
|
||||
if (randColor.is_displayable() && !(L == 0.0 && (u != 0 || v != 0))) {
|
||||
randColorHex = randColor.as("hex");
|
||||
luvSampleFailed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//just sample sRGB if I couldn't sample a random LUV color
|
||||
if (luv_sample_failed) {
|
||||
//console.log("Sampling sRGB");
|
||||
if (luvSampleFailed) {
|
||||
let rgb = [0, 0, 0].map(
|
||||
() => rng.inRange({ min: 0, max: 255, step: 1 }) / 255.0
|
||||
);
|
||||
//console.log(rgb);
|
||||
let rand_color = colorspaces.make_color("sRGB", rgb);
|
||||
rand_color_hex = rand_color.as("hex");
|
||||
let randColor = colorspaces.make_color("sRGB", rgb);
|
||||
randColorHex = randColor.as("hex");
|
||||
}
|
||||
|
||||
return rand_color_hex;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,11 +170,36 @@ function createSimplePaletteGenerator(
|
|||
range(numEntries).map(() => createColor(rng));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
||||
|
||||
const PALETTE_GENERATORS: {
|
||||
[key in RandomPaletteAlgorithm]: RandomPaletteGenerator;
|
||||
} = {
|
||||
RGB: createSimplePaletteGenerator(createRandomRGBColor),
|
||||
CIELUV: createSimplePaletteGenerator(createRandomCIELUVColor),
|
||||
threevals: createTriadPaletteGenerator(create3VColor),
|
||||
huecontrast: createTriadPaletteGenerator(create3HColor),
|
||||
randgrey: createTriadPaletteGenerator(createRandGrey),
|
||||
};
|
||||
|
||||
export const RANDOM_PALETTE_ALGORITHMS = Object.keys(
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { inclusiveRange, NumericInterval, NumericRange } from "./util";
|
||||
import {
|
||||
inclusiveRange,
|
||||
NumericInterval,
|
||||
NumericRange,
|
||||
GaussianDist,
|
||||
} from "./util";
|
||||
|
||||
export type RandomParameters = {
|
||||
modulus: number;
|
||||
|
@ -65,6 +70,19 @@ export class Random {
|
|||
return this.next() * (max - min) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a number from the specified gaussian distribution
|
||||
* from: https://stackoverflow.com/questions/25582882/javascript-math-random-normal-distribution-gaussian-bell-curve
|
||||
*/
|
||||
fromGaussian({ mean, stddev }: GaussianDist, nsamples = 6) {
|
||||
let runtotal = 0;
|
||||
for (var i = 0; i < nsamples; i++) {
|
||||
runtotal += this.next();
|
||||
}
|
||||
|
||||
return (stddev * (runtotal - nsamples / 2)) / (nsamples / 2) + mean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a random item from the given array. If the array is
|
||||
* empty, an exception is thrown.
|
||||
|
|
12
lib/util.ts
12
lib/util.ts
|
@ -37,6 +37,11 @@ export type NumericRange = NumericInterval & {
|
|||
step: number;
|
||||
};
|
||||
|
||||
export type GaussianDist = {
|
||||
mean: number;
|
||||
stddev: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return numbers within the given range, inclusive.
|
||||
*/
|
||||
|
@ -50,6 +55,13 @@ export function inclusiveRange({ min, max, step }: NumericRange): number[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp a number between min and max
|
||||
*/
|
||||
export function clamp(x: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(x, max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array containing the numbers from 0 to one
|
||||
* less than the given value, increasing.
|
||||
|
|
|
@ -4620,6 +4620,11 @@
|
|||
"resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg="
|
||||
},
|
||||
"hsluv": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hsluv/-/hsluv-0.1.0.tgz",
|
||||
"integrity": "sha512-ERcanKLAszD2XN3Vh5r5Szkrv9q0oSTudmP0rkiKAGM/3NMc9FLmMZBB7TSqTaXJfSDBOreYTfjezCOYbRKqlw=="
|
||||
},
|
||||
"html-comment-regex": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz",
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"colorspaces": "^0.1.5",
|
||||
"gh-pages": "^3.1.0",
|
||||
"hsluv": "^0.1.0",
|
||||
"jest": "^26.6.3",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.2.1",
|
||||
|
|
Ładowanie…
Reference in New Issue