Skip to content

Commit

Permalink
feat(color): add declarative range/theme iterators
Browse files Browse the repository at this point in the history
- add ColorRange related types
- add ColorRange presets
- add colorFromRange()
- add colorsFromRange(), colorsFromTheme() iterators
- add analogHSV(), analogRGB() functions
  • Loading branch information
postspectacular committed Dec 29, 2020
1 parent 927202b commit 971d5dc
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 0 deletions.
82 changes: 82 additions & 0 deletions packages/color/src/analog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { FnN } from "@thi.ng/api";
import { clamp01 } from "@thi.ng/math";
import { IRandom, SYSTEM } from "@thi.ng/random";
import { setC4 } from "@thi.ng/vectors";
import type { Color, ReadonlyColor } from "./api";
import { ensureAlpha } from "./internal/ensure-alpha";
import { ensureHue } from "./internal/ensure-hue";

const $analog = (x: number, delta: number, rnd: IRandom, post: FnN = clamp01) =>
delta !== 0 ? post(x + rnd.norm(delta)) : x;

const $alpha = (a: number, delta: number, rnd: IRandom) =>
delta !== 0
? clamp01((a !== undefined ? a : 1) + rnd.norm(delta))
: ensureAlpha(a);

/**
* Similar to {@link analogRGB}. Returns an analog color based on given HSVA
* color,with each channel randomly varied by given delta amounts (and
* optionally given {@link @thi.ng/random#IRandom} PRNG).
*
* @remarks
* By default (unless `deltaS`, `deltaV`, `deltaA` are provided) only the hue of
* the color will be modulated.
*
* @param out
* @param src
* @param deltaH
* @param deltaS
* @param deltaV
* @param deltaA
* @param rnd
*/
export const analogHSV = (
out: Color | null,
src: ReadonlyColor,
deltaH: number,
deltaS = 0,
deltaV = 0,
deltaA = 0,
rnd: IRandom = SYSTEM
) =>
setC4(
out || src,
$analog(src[0], deltaH, rnd, ensureHue),
$analog(src[1], deltaS, rnd),
$analog(src[2], deltaV, rnd),
$alpha(src[3], deltaA, rnd)
);

/**
* Similar to {@link analogHSV}. Returns an analog color based on given RGBA
* color, with each channel randomly varied by given delta amounts (and
* optionally given {@link @thi.ng/random#IRandom} PRNG).
*
* @remarks
* By default the green and blue channel variance will be the same as `deltaR`.
*
* @param out
* @param src
* @param deltaR
* @param deltaG
* @param deltaB
* @param deltaA
* @param rnd
*/
export const analogRGB = (
out: Color | null,
src: ReadonlyColor,
deltaR: number,
deltaG = deltaR,
deltaB = deltaR,
deltaA = 0,
rnd: IRandom = SYSTEM
) =>
setC4(
out || src,
$analog(src[0], deltaR, rnd),
$analog(src[1], deltaG, rnd),
$analog(src[2], deltaB, rnd),
$alpha(src[3], deltaA, rnd)
);
52 changes: 52 additions & 0 deletions packages/color/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ export type CSSColorName =
| "yellow"
| "yellowgreen";

export type ColorRangePreset =
| "light"
| "dark"
| "bright"
| "weak"
| "neutral"
| "fresh"
| "soft"
| "hard"
| "warm"
| "cool"
| "intense";

export type CosineGradientPreset =
| "blue-cyan"
| "blue-magenta-orange"
Expand Down Expand Up @@ -204,3 +217,42 @@ export type ColorDistance = FnU2<ReadonlyColor, number>;
export interface IColor {
readonly mode: ColorMode;
}

export type Range = [number, number];

export interface ColorRange {
/**
* Hue ranges
*/
h?: Range[];
/**
* Saturation ranges
*/
s?: Range[];
/**
* Brightness ranges
*/
v?: Range[];
/**
* Alpha ranges
*/
a?: Range[];
/**
* Black point ranges
*/
b?: Range[];
/**
* White point ranges
*/
w?: Range[];
}

export interface ColorThemePart {
range?: ColorRange | ColorRangePreset;
base?: ReadonlyColor | CSSColorName;
weight?: number;
}

export type ColorThemePartString =
| `${ColorRangePreset} ${CSSColorName} ${number}`
| `${ColorRangePreset | CSSColorName} ${number}`;
190 changes: 190 additions & 0 deletions packages/color/src/color-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { peek } from "@thi.ng/arrays";
import { isString } from "@thi.ng/checks";
import { illegalArgs } from "@thi.ng/errors";
import { IRandom, SYSTEM, weightedRandom } from "@thi.ng/random";
import { analogHSV } from "./analog";
import type {
Color,
ColorRange,
ColorRangePreset,
ColorThemePart,
ColorThemePartString,
Range,
ReadonlyColor,
} from "./api";
import { isBlackHsv, isGrayHsv, isWhiteHsv } from "./checks";
import { ensureAlpha } from "./internal/ensure-alpha";
import { ensureHue } from "./internal/ensure-hue";
import { parseCss } from "./parse-css";
import { rgbaHsva } from "./rgba-hsva";

export interface ColorRangeOpts {
num: number;
variance: number;
rnd: IRandom;
}

export const RANGES: Record<ColorRangePreset, ColorRange> = {
light: {
s: [[0.3, 0.7]],
v: [[0.9, 1]],
b: [[0.15, 0.3]],
w: [[0.3, 1]],
},
dark: {
s: [[0.7, 1]],
v: [[0.15, 0.4]],
b: [[0, 0.5]],
w: [[0.5, 0.75]],
},
bright: {
s: [[0.8, 1]],
v: [[0.8, 1]],
},
weak: {
s: [[0.15, 0.3]],
v: [[0.7, 1]],
b: [[0.2, 0.2]],
w: [[0.2, 1]],
},
neutral: {
s: [[0.25, 0.35]],
v: [[0.3, 0.7]],
b: [[0.15, 0.15]],
w: [[0.9, 1]],
},
fresh: {
s: [[0.4, 0.8]],
v: [[0.8, 1]],
b: [[0.05, 0.3]],
w: [[0.8, 1]],
},
soft: {
s: [[0.2, 0.3]],
v: [[0.6, 0.9]],
b: [[0.05, 0.15]],
w: [[0.6, 0.9]],
},
hard: {
s: [[0.9, 1]],
v: [[0.4, 1]],
},
warm: {
s: [[0.6, 0.9]],
v: [[0.4, 0.9]],
b: [[0.2, 0.2]],
w: [[0.8, 1]],
},
cool: {
s: [[0.05, 0.2]],
v: [[0.9, 1]],
b: [[0, 0.95]],
w: [[0.95, 1]],
},
intense: {
s: [[0.9, 1]],
v: [
[0.2, 0.35],
[0.8, 1],
],
},
};

const FULL: Range[] = [[0, 1]];

const DEFAULT_RANGE: ColorRange = {
h: FULL,
s: FULL,
v: FULL,
b: FULL,
w: FULL,
a: [[1, 1]],
};

const DEFAULT_OPTS: ColorRangeOpts = {
num: Infinity,
variance: 0.025,
rnd: SYSTEM,
};

const $rnd = (ranges: Range[], rnd: IRandom) =>
rnd.minmax(...ranges[rnd.int() % ranges.length]);

export const colorFromRange = (
range: ColorRange,
base?: ReadonlyColor,
opts?: Partial<ColorRangeOpts>
): Color => {
range = { ...DEFAULT_RANGE, ...range };
const { variance, rnd } = { ...DEFAULT_OPTS, ...opts };
let h: number, a: number;
if (base) {
h = base[0];
a = ensureAlpha(base[3]);
if (isBlackHsv(base)) return [h, 0, $rnd(range.b!, rnd), a];
if (isWhiteHsv(base)) return [h, 0, $rnd(range.w!, rnd), a];
if (isGrayHsv(base))
return [
h,
0,
$rnd(rnd.float(1) < 0.5 ? range.b! : range.w!, rnd),
a,
];
h = ensureHue(h + rnd.norm(variance));
} else {
h = $rnd(range.h!, rnd);
a = $rnd(range.a!, rnd);
}
return [h, $rnd(range.s!, rnd), $rnd(range.v!, rnd), a];
};

export function* colorsFromRange(
range: ColorRange,
base?: ReadonlyColor,
opts: Partial<ColorRangeOpts> = {}
) {
let num = opts.num || Infinity;
while (--num >= 0) yield colorFromRange(range, base, opts);
}

const asThemePart = (p: ColorThemePart | ColorThemePartString) => {
if (!isString(p)) return p;
const items = p.split(" ");
let weight = parseFloat(peek(items));
if (isNaN(weight)) {
weight = 1;
} else {
items.pop();
}
return <ColorThemePart>(
(items.length === 2
? { range: items[0], base: items[1], weight }
: items.length === 1
? RANGES[<ColorRangePreset>items[0]]
? { range: items[0], weight }
: { base: items[0], weight }
: illegalArgs(`invalid theme part: "${p}"`))
);
};

export function* colorsFromTheme(
parts: (ColorThemePart | ColorThemePartString)[],
opts: Partial<ColorRangeOpts> = {}
) {
opts = { ...DEFAULT_OPTS, ...opts };
let { num, variance } = opts;
const theme = parts.map(asThemePart);
const choice = weightedRandom(
theme,
theme.map((x) => (x.weight != null ? x.weight : 1))
);
while (--num! >= 0) {
const spec = choice();
const base = isString(spec.base)
? rgbaHsva([], parseCss(spec.base))
: spec.base;
const range = isString(spec.range) ? RANGES[spec.range] : spec.range;
if (range) yield colorFromRange(range, base, opts);
else if (base) yield analogHSV([], base, variance!);
}
}
2 changes: 2 additions & 0 deletions packages/color/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ export * from "./xyza";
export * from "./ycbcr";

export * from "./alpha";
export * from "./analog";
export * from "./checks";
export * from "./clamp";
export * from "./closest-hue";
export * from "./color-range";
export * from "./cosine-gradients";
export * from "./distance";
export * from "./invert";
Expand Down

0 comments on commit 971d5dc

Please sign in to comment.