@roobscoob/vibes 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +254 -2
- package/dist/arclamp.d.ts +34 -0
- package/dist/arclamp.d.ts.map +1 -0
- package/dist/arclamp.js +5 -0
- package/dist/color.d.ts +199 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +491 -0
- package/dist/field.d.ts +94 -0
- package/dist/field.d.ts.map +1 -0
- package/dist/field.js +232 -0
- package/dist/gamut.d.ts +5 -0
- package/dist/gamut.d.ts.map +1 -0
- package/dist/gamut.js +29 -0
- package/dist/gamut.table.d.ts +5 -0
- package/dist/gamut.table.d.ts.map +1 -0
- package/dist/gamut.table.js +153 -0
- package/dist/here.d.ts +103 -0
- package/dist/here.d.ts.map +1 -0
- package/dist/here.js +217 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/lemtone.d.ts +37 -0
- package/dist/lemtone.d.ts.map +1 -0
- package/dist/lemtone.js +158 -0
- package/dist/names.d.ts +5 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +46 -0
- package/dist/parse.d.ts +43 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +208 -0
- package/dist/shape.d.ts +44 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +89 -0
- package/dist/signal.d.ts +26 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +44 -0
- package/dist/sketch.d.ts +21 -0
- package/dist/sketch.d.ts.map +1 -0
- package/dist/sketch.js +89 -0
- package/dist/spaces.d.ts +24 -0
- package/dist/spaces.d.ts.map +1 -0
- package/dist/spaces.js +91 -0
- package/package.json +24 -4
- package/src/arclamp.ts +36 -0
- package/src/color.ts +586 -0
- package/src/field.ts +321 -0
- package/src/gamut.table.ts +156 -0
- package/src/gamut.ts +32 -0
- package/src/here.ts +270 -0
- package/src/index.ts +52 -0
- package/src/lemtone.ts +180 -0
- package/src/names.ts +50 -0
- package/src/parse.ts +205 -0
- package/src/shape.ts +109 -0
- package/src/signal.ts +65 -0
- package/src/sketch.ts +116 -0
- package/src/spaces.ts +102 -0
package/dist/parse.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Colors from text, read as generously as can be: anything CSS takes (hex
|
|
2
|
+
// codes with or without #, names, rgb(), hsl(), hwb(), oklch(), oklab(),
|
|
3
|
+
// lab(), lch(), color()), color temperatures ("2700K", "candlelight"), the
|
|
4
|
+
// lights' own ("lemtone(265 0.3 40)", or "265° C0.30 · 40 lm" as a color
|
|
5
|
+
// prints itself), and any of those with lumens or a fidelity alongside
|
|
6
|
+
// ("#f80 · 120 lm", "2700K @ 400lm").
|
|
7
|
+
//
|
|
8
|
+
// This only reads the text: what it says comes back as a Spec, and color.ts
|
|
9
|
+
// makes the color.
|
|
10
|
+
import { cssNamed, LIGHT_NAMES } from "./names.js";
|
|
11
|
+
const NUMBER = /^([-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(%|deg|rad|grad|turn)?$/;
|
|
12
|
+
function token(text) {
|
|
13
|
+
if (text === "none")
|
|
14
|
+
return { value: 0, unit: "" };
|
|
15
|
+
const m = NUMBER.exec(text);
|
|
16
|
+
return m ? { value: Number(m[1]), unit: (m[2] ?? "") } : null;
|
|
17
|
+
}
|
|
18
|
+
/** An angle in degrees. */
|
|
19
|
+
function angle(t) {
|
|
20
|
+
if (t.unit === "rad")
|
|
21
|
+
return (t.value * 180) / Math.PI;
|
|
22
|
+
if (t.unit === "grad")
|
|
23
|
+
return t.value * 0.9;
|
|
24
|
+
if (t.unit === "turn")
|
|
25
|
+
return t.value * 360;
|
|
26
|
+
return t.value;
|
|
27
|
+
}
|
|
28
|
+
/** A number, with 100% as `hundred`. */
|
|
29
|
+
const scaled = (t, hundred) => (t.unit === "%" ? (t.value / 100) * hundred : t.value);
|
|
30
|
+
/** A fraction 0–1, from a percentage, or a number: 0–1 as it is, past 1 read as a percentage (hsl(240, 100, 50)). */
|
|
31
|
+
const fraction = (t) => (t.unit === "%" || Math.abs(t.value) > 1 ? t.value / 100 : t.value);
|
|
32
|
+
/** Lightness 0–1, from a percentage, or a number: 0–1 as it is, past 1 read as a percentage (oklch(70 0.2 265)). */
|
|
33
|
+
const lightness = fraction;
|
|
34
|
+
/** What `text` says a color is, or null if it can't be read. */
|
|
35
|
+
export function parseColor(text) {
|
|
36
|
+
const s = text.trim().toLowerCase();
|
|
37
|
+
if (!s)
|
|
38
|
+
return null;
|
|
39
|
+
const whole = one(s);
|
|
40
|
+
if (whole)
|
|
41
|
+
return whole;
|
|
42
|
+
// A color, its lumens and its fidelity, in any order: "#f80 · 120 lm", "2700k @ 400lm", "2700k 400lm".
|
|
43
|
+
const found = {};
|
|
44
|
+
for (const part of s.split(/\s*[·@]\s*/)) {
|
|
45
|
+
if (!part || read(part, found))
|
|
46
|
+
continue;
|
|
47
|
+
if (part.includes("("))
|
|
48
|
+
return null;
|
|
49
|
+
// Word by word, a number with the unit after it first ("50 lumens", "fidelity 0.5").
|
|
50
|
+
const words = part.split(/\s+/);
|
|
51
|
+
for (let i = 0; i < words.length; i++) {
|
|
52
|
+
if (i + 1 < words.length && read(`${words[i]} ${words[i + 1]}`, found))
|
|
53
|
+
i++;
|
|
54
|
+
else if (!read(words[i], found))
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!found.spec)
|
|
59
|
+
return null;
|
|
60
|
+
if (found.lm !== undefined)
|
|
61
|
+
found.spec.lm = found.lm;
|
|
62
|
+
if (found.fidelity !== undefined)
|
|
63
|
+
found.spec.fidelity = found.fidelity;
|
|
64
|
+
return found.spec;
|
|
65
|
+
}
|
|
66
|
+
/** One part of a longer text, into `found`: lumens ("400 lm"), a fidelity ("F0.50"), or the color. */
|
|
67
|
+
function read(part, found) {
|
|
68
|
+
let m;
|
|
69
|
+
if ((m = /^([-+]?(?:\d+\.?\d*|\.\d+))\s*(?:lm|lumens?)$/.exec(part))) {
|
|
70
|
+
found.lm = Number(m[1]);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// "f0.5" needs its point, so it isn't taken for a hex code like "f80".
|
|
74
|
+
if ((m = /^(?:f(\d*\.\d+)|fidelity\s*[:=]?\s*(\d*\.?\d+))$/.exec(part))) {
|
|
75
|
+
found.fidelity = Number(m[1] ?? m[2]);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const c = one(part);
|
|
79
|
+
if (!c || found.spec)
|
|
80
|
+
return false;
|
|
81
|
+
found.spec = c;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/** One color, alone. */
|
|
85
|
+
function one(s) {
|
|
86
|
+
if (s === "off" || s === "none" || s === "dark" || s === "transparent")
|
|
87
|
+
return { space: "off" };
|
|
88
|
+
const name = cssNamed(s.replace(/[\s_-]/g, ""));
|
|
89
|
+
if (name)
|
|
90
|
+
return hex(name);
|
|
91
|
+
const kelvin = LIGHT_NAMES[s.replace(/[\s_-]/g, "")];
|
|
92
|
+
if (kelvin)
|
|
93
|
+
return { space: "kelvin", kelvin };
|
|
94
|
+
let m;
|
|
95
|
+
if ((m = /^(\d+(?:\.\d+)?)\s*(?:k|kelvin)$/.exec(s)))
|
|
96
|
+
return { space: "kelvin", kelvin: Number(m[1]) };
|
|
97
|
+
// As a color prints itself: "265° c0.300".
|
|
98
|
+
if ((m = /^([-+]?\d+(?:\.\d+)?)\s*(?:°|deg)\s*c\s*(\d*\.?\d+)$/.exec(s)))
|
|
99
|
+
return { space: "lemtone", hue: Number(m[1]), chroma: Number(m[2]) };
|
|
100
|
+
if (/^#?(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(s))
|
|
101
|
+
return hex(s.replace("#", ""));
|
|
102
|
+
if ((m = /^([a-z][a-z0-9-]*)\((.*)\)$/.exec(s)))
|
|
103
|
+
return fn(m[1], m[2]);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/** Hex digits (3, 4, 6 or 8 of them, the last as alpha) as sRGB. */
|
|
107
|
+
function hex(digits) {
|
|
108
|
+
const full = digits.length <= 4 ? [...digits].map((d) => d + d).join("") : digits;
|
|
109
|
+
const at = (i) => parseInt(full.slice(i, i + 2), 16) / 255;
|
|
110
|
+
const spec = { space: "srgb", rgb: [at(0), at(2), at(4)] };
|
|
111
|
+
if (full.length === 8)
|
|
112
|
+
spec.alpha = at(6);
|
|
113
|
+
return spec;
|
|
114
|
+
}
|
|
115
|
+
/** A function: rgb(…), oklch(…), color(display-p3 …), lemtone(…), … */
|
|
116
|
+
function fn(name, inside) {
|
|
117
|
+
const [main = "", alphaText] = inside.split("/");
|
|
118
|
+
const words = main.trim().split(/[\s,]+/).filter(Boolean);
|
|
119
|
+
let space;
|
|
120
|
+
if (name === "color")
|
|
121
|
+
space = words.shift();
|
|
122
|
+
const t = words.map(token);
|
|
123
|
+
if (t.some((x) => x === null))
|
|
124
|
+
return null;
|
|
125
|
+
const v = t;
|
|
126
|
+
// Old-style rgba(1, 2, 3, 0.5): a fourth number is alpha.
|
|
127
|
+
let alpha;
|
|
128
|
+
if (alphaText !== undefined) {
|
|
129
|
+
const a = token(alphaText.trim());
|
|
130
|
+
if (!a)
|
|
131
|
+
return null;
|
|
132
|
+
alpha = scaled(a, 1);
|
|
133
|
+
}
|
|
134
|
+
else if (v.length === 4 && name !== "lemtone")
|
|
135
|
+
alpha = scaled(v.pop(), 1);
|
|
136
|
+
const spec = make(name, space, v);
|
|
137
|
+
if (spec && alpha !== undefined)
|
|
138
|
+
spec.alpha = alpha;
|
|
139
|
+
return spec;
|
|
140
|
+
}
|
|
141
|
+
function make(name, space, v) {
|
|
142
|
+
const need = (n) => v.length >= n;
|
|
143
|
+
switch (name) {
|
|
144
|
+
case "rgb":
|
|
145
|
+
case "rgba":
|
|
146
|
+
if (!need(3))
|
|
147
|
+
return null;
|
|
148
|
+
return { space: "srgb", rgb: [scaled(v[0], 255) / 255, scaled(v[1], 255) / 255, scaled(v[2], 255) / 255] };
|
|
149
|
+
case "hsl":
|
|
150
|
+
case "hsla":
|
|
151
|
+
if (!need(3))
|
|
152
|
+
return null;
|
|
153
|
+
return { space: "hsl", h: angle(v[0]), s: fraction(v[1]), l: fraction(v[2]) };
|
|
154
|
+
case "hwb":
|
|
155
|
+
if (!need(3))
|
|
156
|
+
return null;
|
|
157
|
+
return { space: "hwb", h: angle(v[0]), w: fraction(v[1]), b: fraction(v[2]) };
|
|
158
|
+
case "oklab":
|
|
159
|
+
if (!need(3))
|
|
160
|
+
return null;
|
|
161
|
+
return { space: "oklab", lab: [lightness(v[0]), scaled(v[1], 0.4), scaled(v[2], 0.4)] };
|
|
162
|
+
case "oklch":
|
|
163
|
+
if (!need(3))
|
|
164
|
+
return null;
|
|
165
|
+
return { space: "oklch", l: lightness(v[0]), c: scaled(v[1], 0.4), h: angle(v[2]) };
|
|
166
|
+
case "lab":
|
|
167
|
+
if (!need(3))
|
|
168
|
+
return null;
|
|
169
|
+
return { space: "lab", lab: [scaled(v[0], 100), scaled(v[1], 125), scaled(v[2], 125)] };
|
|
170
|
+
case "lch":
|
|
171
|
+
if (!need(3))
|
|
172
|
+
return null;
|
|
173
|
+
return { space: "lch", l: scaled(v[0], 100), c: scaled(v[1], 150), h: angle(v[2]) };
|
|
174
|
+
case "color": {
|
|
175
|
+
if (!need(3))
|
|
176
|
+
return null;
|
|
177
|
+
const c = [scaled(v[0], 1), scaled(v[1], 1), scaled(v[2], 1)];
|
|
178
|
+
if (space === "srgb" || space === "srgb-linear" || space === "display-p3")
|
|
179
|
+
return { space, rgb: c };
|
|
180
|
+
if (space === "xyz" || space === "xyz-d65")
|
|
181
|
+
return { space: "xyz", xyz: c };
|
|
182
|
+
if (space === "xyz-d50")
|
|
183
|
+
return { space: "xyz-d50", xyz: c };
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
case "lemtone": {
|
|
187
|
+
if (!need(2))
|
|
188
|
+
return null;
|
|
189
|
+
const spec = { space: "lemtone", hue: angle(v[0]), chroma: v[1].value };
|
|
190
|
+
if (v[2])
|
|
191
|
+
spec.lm = v[2].value;
|
|
192
|
+
if (v[3])
|
|
193
|
+
spec.fidelity = v[3].value;
|
|
194
|
+
return spec;
|
|
195
|
+
}
|
|
196
|
+
case "white":
|
|
197
|
+
case "kelvin": {
|
|
198
|
+
if (!need(1))
|
|
199
|
+
return null;
|
|
200
|
+
const spec = { space: "kelvin", kelvin: v[0].value };
|
|
201
|
+
if (v[1])
|
|
202
|
+
spec.lm = v[1].value;
|
|
203
|
+
return spec;
|
|
204
|
+
}
|
|
205
|
+
default:
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
package/dist/shape.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** The fractional part: 2.25 → 0.25, -0.25 → 0.75. */
|
|
2
|
+
export declare const fract: (x: number) => number;
|
|
3
|
+
export declare const clamp: (x: number, lo?: number, hi?: number) => number;
|
|
4
|
+
/** From a (at t = 0) to b (at t = 1). */
|
|
5
|
+
export declare const lerp: (a: number, b: number, t: number) => number;
|
|
6
|
+
/** x from the range [inLo, inHi] to [outLo, outHi], not clamped. */
|
|
7
|
+
export declare const remap: (x: number, inLo: number, inHi: number, outLo?: number, outHi?: number) => number;
|
|
8
|
+
/** 0 below e0, 1 above e1, and a smooth S between. */
|
|
9
|
+
export declare function smoothstep(e0: number, e1: number, x: number): number;
|
|
10
|
+
/** A smooth wave, one cycle per unit: 0 at whole numbers, 1 halfway between. */
|
|
11
|
+
export declare const wave: (x: number) => number;
|
|
12
|
+
/** Rises from 0 to 1 over each cycle, then drops back at once. */
|
|
13
|
+
export declare const saw: (x: number) => number;
|
|
14
|
+
/** Rises from 0 to 1 over the first half of each cycle and falls back over the second. */
|
|
15
|
+
export declare const tri: (x: number) => number;
|
|
16
|
+
/** 1 for the first `duty` of each cycle, 0 for the rest: a hard on and off. */
|
|
17
|
+
export declare const square: (x: number, duty?: number) => 0 | 1;
|
|
18
|
+
/** Easings, 0 to 1 onto 0 to 1 (cubic). */
|
|
19
|
+
export declare const ease: {
|
|
20
|
+
in: (x: number) => number;
|
|
21
|
+
out: (x: number) => number;
|
|
22
|
+
inOut: (x: number) => number;
|
|
23
|
+
};
|
|
24
|
+
/** A number from 0 to 1 that looks random but is always the same for the same inputs. */
|
|
25
|
+
export declare function hash(...ns: number[]): number;
|
|
26
|
+
/**
|
|
27
|
+
* Smooth noise from 0 to 1, in up to three dimensions: nearby inputs give
|
|
28
|
+
* nearby values, and features are about 1 unit apart. noiseAt(x / 4, y / 4,
|
|
29
|
+
* t / 5) makes blobs about 4 m across that change over about 5 s.
|
|
30
|
+
*/
|
|
31
|
+
export declare function noiseAt(x: number, y?: number, z?: number): number;
|
|
32
|
+
/**
|
|
33
|
+
* How much of something is left after dt seconds, if half goes every
|
|
34
|
+
* `halfLife` seconds: value *= decay(dt, 2) halves it every 2 s, whatever the
|
|
35
|
+
* tick rate.
|
|
36
|
+
*/
|
|
37
|
+
export declare const decay: (dt: number, halfLife: number) => number;
|
|
38
|
+
/**
|
|
39
|
+
* True with the chance of something that happens `perSecond` times a second
|
|
40
|
+
* on average, over the last dt seconds: the same in the room (100 ticks a
|
|
41
|
+
* second) as in the preview (about 30).
|
|
42
|
+
*/
|
|
43
|
+
export declare const chance: (perSecond: number, dt: number) => boolean;
|
|
44
|
+
//# sourceMappingURL=shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"AAMA,sDAAsD;AACtD,eAAO,MAAM,KAAK,MAAO,MAAM,WAAsB,CAAC;AAEtD,eAAO,MAAM,KAAK,MAAO,MAAM,qCAAoD,CAAC;AAEpF,yCAAyC;AACzC,eAAO,MAAM,IAAI,MAAO,MAAM,KAAK,MAAM,KAAK,MAAM,WAAoB,CAAC;AAEzE,oEAAoE;AACpE,eAAO,MAAM,KAAK,MAAO,MAAM,QAAQ,MAAM,QAAQ,MAAM,2CACE,CAAC;AAE9D,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED,gFAAgF;AAChF,eAAO,MAAM,IAAI,MAAO,MAAM,WAAkC,CAAC;AAEjE,kEAAkE;AAClE,eAAO,MAAM,GAAG,MAAO,MAAM,WAAa,CAAC;AAE3C,0FAA0F;AAC1F,eAAO,MAAM,GAAG,MAAO,MAAM,WAAmC,CAAC;AAEjE,+EAA+E;AAC/E,eAAO,MAAM,MAAM,MAAO,MAAM,yBAA0C,CAAC;AAE3E,2CAA2C;AAC3C,eAAO,MAAM,IAAI;IACf,EAAE,MAAM,MAAM;IACd,GAAG,MAAM,MAAM;IACf,KAAK,MAAM,MAAM;CAIlB,CAAC;AAMF,yFAAyF;AACzF,wBAAgB,IAAI,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ5C;AAiBD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,SAAI,EAAE,CAAC,SAAI,GAAG,MAAM,CAavD;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,OAAQ,MAAM,YAAY,MAAM,WAAgD,CAAC;AAEnG;;;;GAIG;AACH,eAAO,MAAM,MAAM,cAAe,MAAM,MAAM,MAAM,YAAkD,CAAC"}
|
package/dist/shape.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Numbers to numbers: waves, easing, ranges, hashing, noise, and time steps.
|
|
2
|
+
// Waves count in cycles: wave(t / 8) goes round once every 8 seconds. They
|
|
3
|
+
// all run 0 to 1, starting at 0, so they make good brightnesses and mixes.
|
|
4
|
+
const TAU = 2 * Math.PI;
|
|
5
|
+
/** The fractional part: 2.25 → 0.25, -0.25 → 0.75. */
|
|
6
|
+
export const fract = (x) => x - Math.floor(x);
|
|
7
|
+
export const clamp = (x, lo = 0, hi = 1) => (x < lo ? lo : x > hi ? hi : x);
|
|
8
|
+
/** From a (at t = 0) to b (at t = 1). */
|
|
9
|
+
export const lerp = (a, b, t) => a + (b - a) * t;
|
|
10
|
+
/** x from the range [inLo, inHi] to [outLo, outHi], not clamped. */
|
|
11
|
+
export const remap = (x, inLo, inHi, outLo = 0, outHi = 1) => outLo + ((x - inLo) / (inHi - inLo || 1)) * (outHi - outLo);
|
|
12
|
+
/** 0 below e0, 1 above e1, and a smooth S between. */
|
|
13
|
+
export function smoothstep(e0, e1, x) {
|
|
14
|
+
const t = clamp((x - e0) / (e1 - e0 || 1));
|
|
15
|
+
return t * t * (3 - 2 * t);
|
|
16
|
+
}
|
|
17
|
+
/** A smooth wave, one cycle per unit: 0 at whole numbers, 1 halfway between. */
|
|
18
|
+
export const wave = (x) => 0.5 - 0.5 * Math.cos(TAU * x);
|
|
19
|
+
/** Rises from 0 to 1 over each cycle, then drops back at once. */
|
|
20
|
+
export const saw = (x) => fract(x);
|
|
21
|
+
/** Rises from 0 to 1 over the first half of each cycle and falls back over the second. */
|
|
22
|
+
export const tri = (x) => 1 - Math.abs(2 * fract(x) - 1);
|
|
23
|
+
/** 1 for the first `duty` of each cycle, 0 for the rest: a hard on and off. */
|
|
24
|
+
export const square = (x, duty = 0.5) => (fract(x) < duty ? 1 : 0);
|
|
25
|
+
/** Easings, 0 to 1 onto 0 to 1 (cubic). */
|
|
26
|
+
export const ease = {
|
|
27
|
+
in: (x) => clamp(x) ** 3,
|
|
28
|
+
out: (x) => 1 - (1 - clamp(x)) ** 3,
|
|
29
|
+
inOut: (x) => {
|
|
30
|
+
const t = clamp(x);
|
|
31
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
// Each number's exact bits, so any number (not only whole ones) hashes.
|
|
35
|
+
const bits = new Float64Array(1);
|
|
36
|
+
const words = new Uint32Array(bits.buffer);
|
|
37
|
+
/** A number from 0 to 1 that looks random but is always the same for the same inputs. */
|
|
38
|
+
export function hash(...ns) {
|
|
39
|
+
let h = 0x9e3779b9;
|
|
40
|
+
for (const n of ns) {
|
|
41
|
+
bits[0] = n === 0 ? 0 : n; // -0 and 0 hash the same
|
|
42
|
+
h = mixBits(h ^ words[0]);
|
|
43
|
+
h = mixBits(h ^ words[1]);
|
|
44
|
+
}
|
|
45
|
+
return (h >>> 0) / 4294967296;
|
|
46
|
+
}
|
|
47
|
+
function mixBits(h) {
|
|
48
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
49
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
50
|
+
return h ^ (h >>> 16);
|
|
51
|
+
}
|
|
52
|
+
// Whole-number lattice hashing for noise: faster than hash() and as good there.
|
|
53
|
+
function lattice(x, y, z) {
|
|
54
|
+
let h = Math.imul(x, 0x27d4eb2d) ^ Math.imul(y, 0x165667b1) ^ Math.imul(z, 0x9e3779b1);
|
|
55
|
+
h = mixBits(h);
|
|
56
|
+
return (h >>> 0) / 4294967296;
|
|
57
|
+
}
|
|
58
|
+
const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
|
|
59
|
+
/**
|
|
60
|
+
* Smooth noise from 0 to 1, in up to three dimensions: nearby inputs give
|
|
61
|
+
* nearby values, and features are about 1 unit apart. noiseAt(x / 4, y / 4,
|
|
62
|
+
* t / 5) makes blobs about 4 m across that change over about 5 s.
|
|
63
|
+
*/
|
|
64
|
+
export function noiseAt(x, y = 0, z = 0) {
|
|
65
|
+
const xi = Math.floor(x);
|
|
66
|
+
const yi = Math.floor(y);
|
|
67
|
+
const zi = Math.floor(z);
|
|
68
|
+
const u = fade(x - xi);
|
|
69
|
+
const v = fade(y - yi);
|
|
70
|
+
const w = fade(z - zi);
|
|
71
|
+
const c = (dx, dy, dz) => lattice(xi + dx, yi + dy, zi + dz);
|
|
72
|
+
const x00 = lerp(c(0, 0, 0), c(1, 0, 0), u);
|
|
73
|
+
const x10 = lerp(c(0, 1, 0), c(1, 1, 0), u);
|
|
74
|
+
const x01 = lerp(c(0, 0, 1), c(1, 0, 1), u);
|
|
75
|
+
const x11 = lerp(c(0, 1, 1), c(1, 1, 1), u);
|
|
76
|
+
return lerp(lerp(x00, x10, v), lerp(x01, x11, v), w);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* How much of something is left after dt seconds, if half goes every
|
|
80
|
+
* `halfLife` seconds: value *= decay(dt, 2) halves it every 2 s, whatever the
|
|
81
|
+
* tick rate.
|
|
82
|
+
*/
|
|
83
|
+
export const decay = (dt, halfLife) => (halfLife > 0 ? 0.5 ** (dt / halfLife) : 0);
|
|
84
|
+
/**
|
|
85
|
+
* True with the chance of something that happens `perSecond` times a second
|
|
86
|
+
* on average, over the last dt seconds: the same in the room (100 ticks a
|
|
87
|
+
* second) as in the preview (about 30).
|
|
88
|
+
*/
|
|
89
|
+
export const chance = (perSecond, dt) => Math.random() < 1 - Math.exp(-perSecond * dt);
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Field } from "./field.ts";
|
|
2
|
+
export interface Every {
|
|
3
|
+
/** How many marks so far: 0, 1, 2, … */
|
|
4
|
+
count: Field<number>;
|
|
5
|
+
/** 0 to 1 from one mark to the next. */
|
|
6
|
+
ramp: Field<number>;
|
|
7
|
+
/** Seconds since the last mark. */
|
|
8
|
+
since: Field<number>;
|
|
9
|
+
/** 0, up to 1 in `attack` seconds after each mark (default 0.01), and back to 0 over `release` (default 0.3). */
|
|
10
|
+
env(options?: {
|
|
11
|
+
attack?: number;
|
|
12
|
+
release?: number;
|
|
13
|
+
}): Field<number>;
|
|
14
|
+
/** The next of these at each mark, round and round. */
|
|
15
|
+
seq<T>(list: readonly T[]): Field<T>;
|
|
16
|
+
}
|
|
17
|
+
export interface Clock {
|
|
18
|
+
readonly bpm: number;
|
|
19
|
+
/** Beats so far (not whole: 2.5 is halfway through the third). */
|
|
20
|
+
beats: Field<number>;
|
|
21
|
+
/** Marks every n beats: every(1) is each beat, every(4) each bar of four. */
|
|
22
|
+
every(n: number): Every;
|
|
23
|
+
}
|
|
24
|
+
/** A clock at `bpm` beats a minute, starting with the sketch. */
|
|
25
|
+
export declare function clock(bpm: number): Clock;
|
|
26
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAQA,OAAO,EAAS,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,KAAK;IACpB,wCAAwC;IACxC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,wCAAwC;IACxC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACpB,mCAAmC;IACnC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,iHAAiH;IACjH,GAAG,CAAC,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACpE,uDAAuD;IACvD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACzB;AAED,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CA+BxC"}
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Signals: fields that only read the time, for wiring into any number a field
|
|
2
|
+
// or source takes. A clock counts beats; every(n) marks each n-th one, and
|
|
3
|
+
// from those come envelopes (a flash on each), sequences (a new value each)
|
|
4
|
+
// and ramps (0 to 1 across each).
|
|
5
|
+
//
|
|
6
|
+
// const beat = clock(96);
|
|
7
|
+
// osc(4).rotate(beat.every(4).seq([0, 45, 90, 135])).color(palettes.dusk).boost(beat.every(1).env(), 0.8)
|
|
8
|
+
import { field } from "./field.js";
|
|
9
|
+
import { fract } from "./shape.js";
|
|
10
|
+
/** A clock at `bpm` beats a minute, starting with the sketch. */
|
|
11
|
+
export function clock(bpm) {
|
|
12
|
+
const beats = field(({ t }) => (t * bpm) / 60);
|
|
13
|
+
return {
|
|
14
|
+
bpm,
|
|
15
|
+
beats,
|
|
16
|
+
every(n) {
|
|
17
|
+
const at = (t) => (t * bpm) / 60 / n;
|
|
18
|
+
const seconds = (60 / bpm) * n;
|
|
19
|
+
const since = field(({ t }) => fract(at(t)) * seconds);
|
|
20
|
+
return {
|
|
21
|
+
count: field(({ t }) => Math.floor(at(t))),
|
|
22
|
+
ramp: field(({ t }) => fract(at(t))),
|
|
23
|
+
since,
|
|
24
|
+
env({ attack = 0.01, release = 0.3 } = {}) {
|
|
25
|
+
return field((here) => {
|
|
26
|
+
const s = since(here);
|
|
27
|
+
if (s < attack)
|
|
28
|
+
return s / attack;
|
|
29
|
+
const k = Math.max(0, 1 - (s - attack) / release);
|
|
30
|
+
return k * k;
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
seq(list) {
|
|
34
|
+
if (list.length === 0)
|
|
35
|
+
throw new Error("seq(): give it at least one value");
|
|
36
|
+
return field(({ t }) => {
|
|
37
|
+
const i = Math.floor(at(t)) % list.length;
|
|
38
|
+
return list[i < 0 ? i + list.length : i];
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/sketch.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ColorInput, type Palette } from "./color.ts";
|
|
2
|
+
import type { Update } from "./arclamp.ts";
|
|
3
|
+
import { Here } from "./here.ts";
|
|
4
|
+
/** What a light should make, here and now; null, undefined or false leaves it as it is below. */
|
|
5
|
+
export type LookResult = ColorInput | null | undefined | false | void;
|
|
6
|
+
/**
|
|
7
|
+
* What a light should make, here and now: a function of here giving a color,
|
|
8
|
+
* one color for every light, or a palette, laid across the room.
|
|
9
|
+
*/
|
|
10
|
+
export type Look = ((here: Here) => LookResult) | ColorInput | Palette;
|
|
11
|
+
export interface SketchOptions {
|
|
12
|
+
/** Old bulbs switch on when asked for this many lumens or more (default 100: about halfway, to the eye, to one that's on). */
|
|
13
|
+
switchOn?: number;
|
|
14
|
+
/** And off when asked for fewer than this (default 70). */
|
|
15
|
+
switchOff?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Your look (a function of here, or a field) as arclamp's update(). */
|
|
18
|
+
export declare function sketch(look: (here: Here) => LookResult, options?: SketchOptions): Update;
|
|
19
|
+
/** One color for every light, or a palette laid across the room, as arclamp's update(). */
|
|
20
|
+
export declare function sketch(look: ColorInput | Palette, options?: SketchOptions): Update;
|
|
21
|
+
//# sourceMappingURL=sketch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sketch.d.ts","sourceRoot":"","sources":["../src/sketch.ts"],"names":[],"mappings":"AAeA,OAAO,EAA2B,KAAK,UAAU,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,KAAK,EAAa,MAAM,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAW,IAAI,EAAiC,MAAM,WAAW,CAAC;AAKzE,iGAAiG;AACjG,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,UAAU,CAAC,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,aAAa;IAC5B,8HAA8H;IAC9H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD,wEAAwE;AACxE,wBAAgB,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;AAC1F,2FAA2F;AAC3F,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC"}
|
package/dist/sketch.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// sketch(): your look as arclamp's update().
|
|
2
|
+
//
|
|
3
|
+
// A look is a function of here (your own, or a field), or one color for
|
|
4
|
+
// every light (a palette is laid across the room, left to right). Each tick
|
|
5
|
+
// sketch() asks it what every light should make now, and turns that into
|
|
6
|
+
// channels: lemtone channels for color lights, and on or off for old bulbs
|
|
7
|
+
// (on at `switchOn` lumens or more, off below `switchOff`, and left as they
|
|
8
|
+
// were between, so a pattern hovering near the line doesn't chatter).
|
|
9
|
+
//
|
|
10
|
+
// Anything a look gives that says a color will do: a Color, "#f80",
|
|
11
|
+
// "2700K", { r, g, b }, … Null, undefined or false leaves the light as it is
|
|
12
|
+
// below. Something that isn't a color, or a color that isn't all numbers,
|
|
13
|
+
// leaves it as it is too, with a warning (once for each kind of mistake), so
|
|
14
|
+
// one slip doesn't stop the sketch.
|
|
15
|
+
import { Color, color, isPalette } from "./color.js";
|
|
16
|
+
import { colorOf, Here, roomOf, sameLayout } from "./here.js";
|
|
17
|
+
// A tick after a long gap (the sketch paused, or held up) counts as at most this long.
|
|
18
|
+
const MAX_DT = 0.25;
|
|
19
|
+
export function sketch(look, options = {}) {
|
|
20
|
+
const ask = lookOf(look);
|
|
21
|
+
const switchOn = options.switchOn ?? 100;
|
|
22
|
+
const switchOff = Math.min(options.switchOff ?? 70, switchOn);
|
|
23
|
+
let room = null;
|
|
24
|
+
let last = null;
|
|
25
|
+
const on = new Map();
|
|
26
|
+
const warned = new Set();
|
|
27
|
+
const warn = (kind, text) => {
|
|
28
|
+
if (warned.has(kind))
|
|
29
|
+
return;
|
|
30
|
+
warned.add(kind);
|
|
31
|
+
console.warn(`${text} (warning once)`);
|
|
32
|
+
};
|
|
33
|
+
return (prev, nowMs, initialMs) => {
|
|
34
|
+
const list = Array.isArray(prev?.lights) ? prev.lights : [];
|
|
35
|
+
if (!room || !sameLayout(prev, room))
|
|
36
|
+
room = roomOf(prev, room);
|
|
37
|
+
const t = (nowMs - initialMs) / 1000;
|
|
38
|
+
const dt = last === null ? 0 : Math.min(MAX_DT, Math.max(0, (nowMs - last) / 1000));
|
|
39
|
+
last = nowMs;
|
|
40
|
+
const lights = [];
|
|
41
|
+
for (let i = 0; i < list.length; i++) {
|
|
42
|
+
const light = room.lights[i];
|
|
43
|
+
light.below = colorOf(list[i], light.kind);
|
|
44
|
+
const v = ask(new Here(light.x, light.y, t, dt, nowMs, light, room));
|
|
45
|
+
if (v === null || v === undefined || v === false)
|
|
46
|
+
continue;
|
|
47
|
+
let c;
|
|
48
|
+
try {
|
|
49
|
+
c = v instanceof Color ? v : color(v);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (typeof v === "number") {
|
|
53
|
+
warn("number", `light ${light.id} was given ${v}, and a number is read as a color temperature in kelvin; for a pattern from 0 to 1, color it first, like osc(3).color(palettes.dusk)`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
warn("unreadable", `light ${light.id} was given something that isn't a color, so it's left as it is: ${err.message}`);
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (light.kind === "switch") {
|
|
61
|
+
if (!Number.isFinite(c.lm)) {
|
|
62
|
+
warn("nan", `light ${light.id} was given a color that isn't all numbers, ${JSON.stringify(c)}, so it's left as it is`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const was = on.get(light.id) ?? c.lm >= switchOn;
|
|
66
|
+
const now = c.lm >= switchOn ? true : c.lm < switchOff ? false : was;
|
|
67
|
+
on.set(light.id, now);
|
|
68
|
+
lights.push({ id: light.id, channels: { on: now ? 1 : 0 } });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const channels = c.channels;
|
|
72
|
+
if (!Number.isFinite(channels.a) || !Number.isFinite(channels.b) || !Number.isFinite(channels.brightness) || !Number.isFinite(channels.fidelity)) {
|
|
73
|
+
warn("nan", `light ${light.id} was given a color that isn't all numbers, ${JSON.stringify(c)}, so it's left as it is`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
lights.push({ id: light.id, channels });
|
|
77
|
+
}
|
|
78
|
+
return { lights };
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The look as a function of here. One color is the same everywhere (and said wrong, an error now, not each tick). */
|
|
82
|
+
function lookOf(look) {
|
|
83
|
+
if (isPalette(look))
|
|
84
|
+
return (here) => look(here.u);
|
|
85
|
+
if (typeof look === "function")
|
|
86
|
+
return look;
|
|
87
|
+
const c = color(look);
|
|
88
|
+
return () => c;
|
|
89
|
+
}
|
package/dist/spaces.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Vec3 } from "./lemtone.ts";
|
|
2
|
+
/** The light a screen's white stands for, in lumens. */
|
|
3
|
+
export declare const SCREEN_WHITE_LM = 1000;
|
|
4
|
+
export declare const linearSrgbToXyz: (rgb: Vec3) => Vec3;
|
|
5
|
+
export declare const xyzToLinearSrgb: (X: Vec3) => Vec3;
|
|
6
|
+
/** sRGB's gamma, undone (extended past 0 and 1 the same way both sides, as CSS does). */
|
|
7
|
+
export declare function decode(v: number): number;
|
|
8
|
+
/** sRGB's gamma. */
|
|
9
|
+
export declare function encode(v: number): number;
|
|
10
|
+
export declare const linearP3ToXyz: (rgb: Vec3) => Vec3;
|
|
11
|
+
export declare const xyzD50ToXyz: (X: Vec3) => Vec3;
|
|
12
|
+
/** CIE Lab (L 0–100) as XYZ (D65). */
|
|
13
|
+
export declare function labToXyz(L: number, a: number, b: number): Vec3;
|
|
14
|
+
/** HSL (hue in degrees, saturation and lightness 0–1) as gamma-encoded sRGB (CSS Color 4). */
|
|
15
|
+
export declare function hslToSrgb(h: number, s: number, l: number): Vec3;
|
|
16
|
+
/** HWB (hue in degrees, whiteness and blackness 0–1) as gamma-encoded sRGB (CSS Color 4). */
|
|
17
|
+
export declare function hwbToSrgb(h: number, w: number, b: number): Vec3;
|
|
18
|
+
/** Gamma-encoded sRGB as HSL (CSS Color 4): hue in degrees (0 for grays), saturation and lightness 0–1. */
|
|
19
|
+
export declare function srgbToHsl(r: number, g: number, b: number): {
|
|
20
|
+
h: number;
|
|
21
|
+
s: number;
|
|
22
|
+
l: number;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=spaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spaces.d.ts","sourceRoot":"","sources":["../src/spaces.ts"],"names":[],"mappings":"AASA,OAAO,EAA4B,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAEnE,wDAAwD;AACxD,eAAO,MAAM,eAAe,OAAO,CAAC;AAcpC,eAAO,MAAM,eAAe,QAAS,IAAI,KAAG,IAAqC,CAAC;AAClF,eAAO,MAAM,eAAe,MAAO,IAAI,KAAG,IAAmC,CAAC;AAE9E,yFAAyF;AACzF,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAGxC;AAED,oBAAoB;AACpB,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAGxC;AAQD,eAAO,MAAM,aAAa,QAAS,IAAI,KAAG,IAAyB,CAAC;AASpE,eAAO,MAAM,WAAW,MAAO,IAAI,KAAG,IAAwB,CAAC;AAE/D,sCAAsC;AACtC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAU9D;AAED,8FAA8F;AAC9F,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAO/D;AAED,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAM/D;AAED,2GAA2G;AAC3G,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAU9F"}
|
package/dist/spaces.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The color spaces people know, to and from the light they stand for.
|
|
2
|
+
//
|
|
3
|
+
// They're all screens' spaces, so their brightness is relative to a screen's
|
|
4
|
+
// white. Here that white is a fixed amount of light, 1000 lm (lemtone's
|
|
5
|
+
// brightness 1.0), the same on every light: rgb(1, 1, 1) asks for 1000 lm,
|
|
6
|
+
// and a screen's full blue, #0000ff, for about 72, as dim next to white as on
|
|
7
|
+
// a screen. Everything goes through XYZ (D65, Y = 1 for that white), with the
|
|
8
|
+
// matrices CSS Color 4 uses.
|
|
9
|
+
import { lmsToXyz, mul3, xyzToLms } from "./lemtone.js";
|
|
10
|
+
/** The light a screen's white stands for, in lumens. */
|
|
11
|
+
export const SCREEN_WHITE_LM = 1000;
|
|
12
|
+
// Linear sRGB <-> OKLab's LMS (Björn Ottosson's), so sRGB and OKLab agree exactly.
|
|
13
|
+
const SRGB_LMS = [
|
|
14
|
+
[0.4122214708, 0.5363325363, 0.0514459929],
|
|
15
|
+
[0.2119034982, 0.6806995451, 0.1073969566],
|
|
16
|
+
[0.0883024619, 0.2817188376, 0.6299787005],
|
|
17
|
+
];
|
|
18
|
+
const LMS_SRGB = [
|
|
19
|
+
[4.0767416621, -3.3077115913, 0.2309699292],
|
|
20
|
+
[-1.2684380046, 2.6097574011, -0.3413193965],
|
|
21
|
+
[-0.0041960863, -0.7034186147, 1.707614701],
|
|
22
|
+
];
|
|
23
|
+
export const linearSrgbToXyz = (rgb) => lmsToXyz(mul3(SRGB_LMS, rgb));
|
|
24
|
+
export const xyzToLinearSrgb = (X) => mul3(LMS_SRGB, xyzToLms(X));
|
|
25
|
+
/** sRGB's gamma, undone (extended past 0 and 1 the same way both sides, as CSS does). */
|
|
26
|
+
export function decode(v) {
|
|
27
|
+
const x = Math.abs(v);
|
|
28
|
+
return Math.sign(v) * (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4);
|
|
29
|
+
}
|
|
30
|
+
/** sRGB's gamma. */
|
|
31
|
+
export function encode(v) {
|
|
32
|
+
const x = Math.abs(v);
|
|
33
|
+
return Math.sign(v) * (x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055);
|
|
34
|
+
}
|
|
35
|
+
// Linear Display P3 -> XYZ (CSS Color 4).
|
|
36
|
+
const P3_XYZ = [
|
|
37
|
+
[0.48657094864821626, 0.26566769316909294, 0.1982172852343625],
|
|
38
|
+
[0.22897456406974884, 0.6917385218365062, 0.079286914093745],
|
|
39
|
+
[0, 0.045113381858902575, 1.0439443689009757],
|
|
40
|
+
];
|
|
41
|
+
export const linearP3ToXyz = (rgb) => mul3(P3_XYZ, rgb);
|
|
42
|
+
// CIE Lab and LCh are relative to D50; back to D65 by Bradford (CSS Color 4).
|
|
43
|
+
const D50 = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585];
|
|
44
|
+
const D50_D65 = [
|
|
45
|
+
[0.955473421488075, -0.02309845494876471, 0.06325924320057072],
|
|
46
|
+
[-0.0283697093338637, 1.0099953980813041, 0.021041441191917323],
|
|
47
|
+
[0.012314014864481998, -0.020507649298898964, 1.330365926242124],
|
|
48
|
+
];
|
|
49
|
+
export const xyzD50ToXyz = (X) => mul3(D50_D65, X);
|
|
50
|
+
/** CIE Lab (L 0–100) as XYZ (D65). */
|
|
51
|
+
export function labToXyz(L, a, b) {
|
|
52
|
+
const k = 24389 / 27;
|
|
53
|
+
const e = 216 / 24389;
|
|
54
|
+
const f1 = (L + 16) / 116;
|
|
55
|
+
const f0 = a / 500 + f1;
|
|
56
|
+
const f2 = f1 - b / 200;
|
|
57
|
+
const x = f0 ** 3 > e ? f0 ** 3 : (116 * f0 - 16) / k;
|
|
58
|
+
const y = L > k * e ? f1 ** 3 : L / k;
|
|
59
|
+
const z = f2 ** 3 > e ? f2 ** 3 : (116 * f2 - 16) / k;
|
|
60
|
+
return xyzD50ToXyz([x * D50[0], y, z * D50[2]]);
|
|
61
|
+
}
|
|
62
|
+
/** HSL (hue in degrees, saturation and lightness 0–1) as gamma-encoded sRGB (CSS Color 4). */
|
|
63
|
+
export function hslToSrgb(h, s, l) {
|
|
64
|
+
const hue = ((h % 360) + 360) % 360;
|
|
65
|
+
const f = (n) => {
|
|
66
|
+
const k = (n + hue / 30) % 12;
|
|
67
|
+
return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
|
|
68
|
+
};
|
|
69
|
+
return [f(0), f(8), f(4)];
|
|
70
|
+
}
|
|
71
|
+
/** HWB (hue in degrees, whiteness and blackness 0–1) as gamma-encoded sRGB (CSS Color 4). */
|
|
72
|
+
export function hwbToSrgb(h, w, b) {
|
|
73
|
+
if (w + b >= 1) {
|
|
74
|
+
const gray = w / (w + b);
|
|
75
|
+
return [gray, gray, gray];
|
|
76
|
+
}
|
|
77
|
+
return hslToSrgb(h, 1, 0.5).map((v) => v * (1 - w - b) + w);
|
|
78
|
+
}
|
|
79
|
+
/** Gamma-encoded sRGB as HSL (CSS Color 4): hue in degrees (0 for grays), saturation and lightness 0–1. */
|
|
80
|
+
export function srgbToHsl(r, g, b) {
|
|
81
|
+
const max = Math.max(r, g, b);
|
|
82
|
+
const min = Math.min(r, g, b);
|
|
83
|
+
const l = (max + min) / 2;
|
|
84
|
+
const d = max - min;
|
|
85
|
+
if (d === 0)
|
|
86
|
+
return { h: 0, s: 0, l };
|
|
87
|
+
const s = l === 0 || l === 1 ? 0 : (max - l) / Math.min(l, 1 - l);
|
|
88
|
+
let h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
89
|
+
h *= 60;
|
|
90
|
+
return { h, s, l };
|
|
91
|
+
}
|