From 7003daeebf4ce4bc287d901fa8004a2d15151773 Mon Sep 17 00:00:00 2001 From: mittimithai Date: Fri, 30 Apr 2021 04:15:38 -0700 Subject: [PATCH] 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. --- lib/random-colors.ts | 150 ++++++++++++++++++++++++++++++++++++++----- lib/random.ts | 20 +++++- lib/util.ts | 12 ++++ package-lock.json | 5 ++ package.json | 1 + 5 files changed, 170 insertions(+), 18 deletions(-) diff --git a/lib/random-colors.ts b/lib/random-colors.ts index 6eba821..0f18cc1 100644 --- a/lib/random-colors.ts +++ b/lib/random-colors.ts @@ -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( diff --git a/lib/random.ts b/lib/random.ts index 3757e31..e6c1552 100644 --- a/lib/random.ts +++ b/lib/random.ts @@ -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. diff --git a/lib/util.ts b/lib/util.ts index 2b4272e..14483f2 100644 --- a/lib/util.ts +++ b/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. diff --git a/package-lock.json b/package-lock.json index 249dc2f..8eccbba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 899dcb5..2a1aa75 100644 --- a/package.json +++ b/package.json @@ -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",