@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -2
  3. package/dist/arclamp.d.ts +34 -0
  4. package/dist/arclamp.d.ts.map +1 -0
  5. package/dist/arclamp.js +5 -0
  6. package/dist/color.d.ts +199 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +491 -0
  9. package/dist/field.d.ts +94 -0
  10. package/dist/field.d.ts.map +1 -0
  11. package/dist/field.js +232 -0
  12. package/dist/gamut.d.ts +5 -0
  13. package/dist/gamut.d.ts.map +1 -0
  14. package/dist/gamut.js +29 -0
  15. package/dist/gamut.table.d.ts +5 -0
  16. package/dist/gamut.table.d.ts.map +1 -0
  17. package/dist/gamut.table.js +153 -0
  18. package/dist/here.d.ts +103 -0
  19. package/dist/here.d.ts.map +1 -0
  20. package/dist/here.js +217 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +8 -0
  24. package/dist/lemtone.d.ts +37 -0
  25. package/dist/lemtone.d.ts.map +1 -0
  26. package/dist/lemtone.js +158 -0
  27. package/dist/names.d.ts +5 -0
  28. package/dist/names.d.ts.map +1 -0
  29. package/dist/names.js +46 -0
  30. package/dist/parse.d.ts +43 -0
  31. package/dist/parse.d.ts.map +1 -0
  32. package/dist/parse.js +208 -0
  33. package/dist/shape.d.ts +44 -0
  34. package/dist/shape.d.ts.map +1 -0
  35. package/dist/shape.js +89 -0
  36. package/dist/signal.d.ts +26 -0
  37. package/dist/signal.d.ts.map +1 -0
  38. package/dist/signal.js +44 -0
  39. package/dist/sketch.d.ts +21 -0
  40. package/dist/sketch.d.ts.map +1 -0
  41. package/dist/sketch.js +89 -0
  42. package/dist/spaces.d.ts +24 -0
  43. package/dist/spaces.d.ts.map +1 -0
  44. package/dist/spaces.js +91 -0
  45. package/package.json +24 -4
  46. package/src/arclamp.ts +36 -0
  47. package/src/color.ts +586 -0
  48. package/src/field.ts +321 -0
  49. package/src/gamut.table.ts +156 -0
  50. package/src/gamut.ts +32 -0
  51. package/src/here.ts +270 -0
  52. package/src/index.ts +52 -0
  53. package/src/lemtone.ts +180 -0
  54. package/src/names.ts +50 -0
  55. package/src/parse.ts +205 -0
  56. package/src/shape.ts +109 -0
  57. package/src/signal.ts +65 -0
  58. package/src/sketch.ts +116 -0
  59. package/src/spaces.ts +102 -0
package/dist/here.js ADDED
@@ -0,0 +1,217 @@
1
+ // Here: one light, now. Everything a sketch is made of (its functions, its
2
+ // fields, and their parameters) takes a `here` and gives back what that
3
+ // light should do. A field's transforms (rotate, kaleid, delay, ...) change
4
+ // where and when it looks (x, y, t), never which light it is (light).
5
+ //
6
+ // Positions are in meters from the building's top-left wall corner (floors
7
+ // share one frame), and u, v run 0 to 1 across the room the lights fill.
8
+ //
9
+ // What arclamp sends is read leniently: a light with no channels, coords that
10
+ // aren't coords (older arclamps sent ""), or tags that aren't a list is taken
11
+ // as having none.
12
+ import { Color, off } from "./color.js";
13
+ import { LUMENS_PER_BRIGHTNESS } from "./lemtone.js";
14
+ import { hash } from "./shape.js";
15
+ export class Light {
16
+ id;
17
+ /** "color" for color bulbs and panels; "switch" for old bulbs, which only go on and off. */
18
+ kind;
19
+ tags;
20
+ /** Whether it's on the plan; one that isn't sits at the room's center. */
21
+ placed;
22
+ floor;
23
+ /** Meters from the building's left wall. */
24
+ x;
25
+ /** Meters from the building's top wall. */
26
+ y;
27
+ /** The rail it hangs on, or null. */
28
+ rail = null;
29
+ /** A number from 0 to 1 of its own, the same every time: for giving each light its own beat or shade. */
30
+ random;
31
+ /** What the layers below this sketch are making it do. */
32
+ below = off;
33
+ /** Its own memory, kept from one tick to the next. */
34
+ state = {};
35
+ constructor(p, x, y) {
36
+ const coords = coordsOf(p);
37
+ this.id = p.id;
38
+ this.kind = isSwitch(p) ? "switch" : "color";
39
+ this.tags = Object.freeze([...tagsOf(p)]);
40
+ this.placed = !!coords;
41
+ this.floor = coords?.floor ?? null;
42
+ this.x = x;
43
+ this.y = y;
44
+ this.random = hash(p.id, 0x5eed);
45
+ }
46
+ has(tag) {
47
+ return this.tags.includes(tag);
48
+ }
49
+ }
50
+ export class Room {
51
+ /** Every light, in arclamp's order. */
52
+ lights;
53
+ rails;
54
+ /** The left and top edges of the room the lights fill, in meters. */
55
+ x0;
56
+ y0;
57
+ /** How wide (left to right) and deep (top to bottom) that room is, in meters. */
58
+ width;
59
+ depth;
60
+ center;
61
+ byId;
62
+ railsById;
63
+ constructor(lights, rails, bounds) {
64
+ this.lights = lights;
65
+ this.rails = rails;
66
+ this.x0 = bounds.x0;
67
+ this.y0 = bounds.y0;
68
+ this.width = bounds.width;
69
+ this.depth = bounds.depth;
70
+ this.center = Object.freeze({ x: bounds.x0 + bounds.width / 2, y: bounds.y0 + bounds.depth / 2 });
71
+ this.byId = new Map(lights.map((l) => [l.id, l]));
72
+ this.railsById = new Map(rails.map((r) => [r.id, r]));
73
+ }
74
+ light(id) {
75
+ return this.byId.get(id);
76
+ }
77
+ rail(id) {
78
+ return this.railsById.get(id);
79
+ }
80
+ /** The lights with this tag. */
81
+ tagged(tag) {
82
+ return this.lights.filter((l) => l.has(tag));
83
+ }
84
+ }
85
+ export class Here {
86
+ /** Where it's looking, in meters (the light's own place, unless a transform moved it). */
87
+ x;
88
+ y;
89
+ /** Seconds since the sketch started (unless a transform moved it). */
90
+ t;
91
+ /** Seconds since the last tick: about 0.01 in the room, 0.03 in the preview. */
92
+ dt;
93
+ /** The clock, in milliseconds since 1970: for things that follow the time of day. */
94
+ now;
95
+ light;
96
+ room;
97
+ constructor(x, y, t, dt, now, light, room) {
98
+ this.x = x;
99
+ this.y = y;
100
+ this.t = t;
101
+ this.dt = dt;
102
+ this.now = now;
103
+ this.light = light;
104
+ this.room = room;
105
+ }
106
+ /** 0 at the room's left edge, 1 at its right. */
107
+ get u() {
108
+ return (this.x - this.room.x0) / this.room.width;
109
+ }
110
+ /** 0 at the room's top edge, 1 at its bottom. */
111
+ get v() {
112
+ return (this.y - this.room.y0) / this.room.depth;
113
+ }
114
+ /** The same light, looking from somewhere else, or at another time. */
115
+ at(p) {
116
+ return new Here(p.x ?? this.x, p.y ?? this.y, p.t ?? this.t, this.dt, this.now, this.light, this.room);
117
+ }
118
+ }
119
+ // ---- reading arclamp's lights -------------------------------------------------------
120
+ const NO_TAGS = Object.freeze([]);
121
+ const finite = (v) => typeof v === "number" && Number.isFinite(v);
122
+ /** A light's coords, if they are some. */
123
+ function coordsOf(p) {
124
+ const c = p.coords;
125
+ return c && typeof c === "object" && finite(c.mm_x) && finite(c.mm_y) ? c : null;
126
+ }
127
+ /** A light's rail, if it has one. */
128
+ function railOf(p) {
129
+ const r = p.rail;
130
+ return r && typeof r === "object" && (typeof r.id === "string" || typeof r.id === "number") && finite(r.mm_along)
131
+ ? { id: String(r.id), mm_along: r.mm_along }
132
+ : null;
133
+ }
134
+ function tagsOf(p) {
135
+ return Array.isArray(p.tags) ? p.tags.filter((t) => typeof t === "string") : NO_TAGS;
136
+ }
137
+ const channelsOf = (p) => (p.channels && typeof p.channels === "object" ? p.channels : {});
138
+ const isSwitch = (p) => "on" in channelsOf(p);
139
+ // The layout each room was made from, to tell when arclamp's has changed.
140
+ const layouts = new WeakMap();
141
+ const snapshot = (p) => {
142
+ const c = coordsOf(p);
143
+ return { id: p.id, switch: isSwitch(p), coords: c && { ...c }, rail: railOf(p), tags: [...tagsOf(p)] };
144
+ };
145
+ /** The room in `prev`, keeping each light's memory from `before` (lights move, and come and go). */
146
+ export function roomOf(prev, before) {
147
+ const list = Array.isArray(prev?.lights) ? prev.lights : [];
148
+ const placed = list.map(coordsOf).filter((c) => !!c);
149
+ const xs = placed.map((c) => c.mm_x / 1000);
150
+ const ys = placed.map((c) => c.mm_y / 1000);
151
+ const x0 = xs.length ? Math.min(...xs) : 0;
152
+ const y0 = ys.length ? Math.min(...ys) : 0;
153
+ const width = Math.max(1, xs.length ? Math.max(...xs) - x0 : 0);
154
+ const depth = Math.max(1, ys.length ? Math.max(...ys) - y0 : 0);
155
+ const cx = x0 + width / 2;
156
+ const cy = y0 + depth / 2;
157
+ const lights = list.map((p) => {
158
+ const c = coordsOf(p);
159
+ const light = new Light(p, c ? c.mm_x / 1000 : cx, c ? c.mm_y / 1000 : cy);
160
+ const old = before?.light(p.id);
161
+ if (old)
162
+ light.state = old.state;
163
+ return light;
164
+ });
165
+ const onRails = new Map();
166
+ list.forEach((p, i) => {
167
+ const rail = railOf(p);
168
+ if (!rail)
169
+ return;
170
+ const entries = onRails.get(rail.id) ?? [];
171
+ entries.push({ light: lights[i], along: rail.mm_along / 1000 });
172
+ onRails.set(rail.id, entries);
173
+ });
174
+ const rails = [];
175
+ for (const [id, entries] of onRails) {
176
+ entries.sort((a, b) => a.along - b.along);
177
+ const length = entries[entries.length - 1].along;
178
+ entries.forEach(({ light, along }, index) => {
179
+ light.rail = { id, along, t: length > 0 ? along / length : 0, index, count: entries.length };
180
+ });
181
+ rails.push({ id, lights: entries.map((e) => e.light), length });
182
+ }
183
+ const room = new Room(lights, rails, { x0, y0, width, depth });
184
+ layouts.set(room, list.map(snapshot));
185
+ return room;
186
+ }
187
+ /** Whether `prev`'s lights are the ones `room` was made from, in the same places. */
188
+ export function sameLayout(prev, room) {
189
+ const known = layouts.get(room);
190
+ const list = Array.isArray(prev?.lights) ? prev.lights : [];
191
+ if (!known || known.length !== list.length)
192
+ return false;
193
+ for (let i = 0; i < known.length; i++) {
194
+ const a = known[i];
195
+ const p = list[i];
196
+ if (a.id !== p.id || a.switch !== isSwitch(p))
197
+ return false;
198
+ const c = coordsOf(p);
199
+ if (!(a.coords === c || (a.coords && c && a.coords.mm_x === c.mm_x && a.coords.mm_y === c.mm_y && a.coords.floor === c.floor)))
200
+ return false;
201
+ const r = railOf(p);
202
+ if (!(a.rail === r || (a.rail && r && a.rail.id === r.id && a.rail.mm_along === r.mm_along)))
203
+ return false;
204
+ const tags = tagsOf(p);
205
+ if (a.tags.length !== tags.length || a.tags.some((t, k) => t !== tags[k]))
206
+ return false;
207
+ }
208
+ return true;
209
+ }
210
+ /** What a light's channels say it's making, as a color: an old bulb that's on is a warm 800 lm. */
211
+ export function colorOf(p, kind) {
212
+ const ch = channelsOf(p);
213
+ const n = (k, d) => (finite(ch[k]) ? ch[k] : d);
214
+ if (kind === "switch")
215
+ return Color.from({ on: n("on", 0) });
216
+ return new Color(n("a", 0), n("b", 0), n("brightness", 0) * LUMENS_PER_BRIGHTNESS, n("fidelity", 1));
217
+ }
@@ -0,0 +1,8 @@
1
+ export type { Coords, Out, Prev, PrevLight, Update } from "./arclamp.ts";
2
+ export { br30, Color, color, hex, hsl, isPalette, lemtone, off, oklab, oklch, palette, palettes, rgb, toPalette, white, xy, type Bulb, type ColorChanges, type ColorInput, type ColorLike, type Palette, type Stop, } from "./color.ts";
3
+ export { Here, Light, Room, type Rail, type RailSpot } from "./here.ts";
4
+ export { delay, field, gradient, isField, kaleid, move, noise, osc, rings, rotate, scale, solid, speed, voronoi, warp, type ColorParam, type Field, type Param, } from "./field.ts";
5
+ export { clock, type Clock, type Every } from "./signal.ts";
6
+ export { chance, clamp, decay, ease, fract, hash, lerp, noiseAt, remap, saw, smoothstep, square, tri, wave } from "./shape.ts";
7
+ export { sketch, type Look, type LookResult, type SketchOptions } from "./sketch.ts";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EACL,IAAI,EACJ,KAAK,EACL,KAAK,EACL,GAAG,EACH,GAAG,EACH,SAAS,EACT,OAAO,EACP,GAAG,EACH,KAAK,EACL,KAAK,EACL,OAAO,EACP,QAAQ,EACR,GAAG,EACH,SAAS,EACT,KAAK,EACL,EAAE,EACF,KAAK,IAAI,EACT,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,KAAK,IAAI,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,MAAM,EACN,IAAI,EACJ,KAAK,EACL,GAAG,EACH,KAAK,EACL,MAAM,EACN,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,EACJ,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,KAAK,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAC/H,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // @roobscoob/vibes: color, light and time for sketches on the lights at the
2
+ // Recurse Center. See README.md.
3
+ export { br30, Color, color, hex, hsl, isPalette, lemtone, off, oklab, oklch, palette, palettes, rgb, toPalette, white, xy, } from "./color.js";
4
+ export { Here, Light, Room } from "./here.js";
5
+ export { delay, field, gradient, isField, kaleid, move, noise, osc, rings, rotate, scale, solid, speed, voronoi, warp, } from "./field.js";
6
+ export { clock } from "./signal.js";
7
+ export { chance, clamp, decay, ease, fract, hash, lerp, noiseAt, remap, saw, smoothstep, square, tri, wave } from "./shape.js";
8
+ export { sketch } from "./sketch.js";
@@ -0,0 +1,37 @@
1
+ export type Vec3 = [number, number, number];
2
+ export type XY = [number, number];
3
+ /** What brightness 1.0 means (rclamp sink.LumensPerBrightness). */
4
+ export declare const LUMENS_PER_BRIGHTNESS = 1000;
5
+ export declare const mul3: (m: number[][], v: Vec3) => Vec3;
6
+ export declare function xyzToOklab(X: Vec3): Vec3;
7
+ /** XYZ as OKLab's LMS (before the cube root). */
8
+ export declare const xyzToLms: (X: Vec3) => Vec3;
9
+ /** OKLab's LMS (before the cube root) as XYZ. */
10
+ export declare const lmsToXyz: (lms: Vec3) => Vec3;
11
+ /** An OKLab color (L, a, b) as XYZ. */
12
+ export declare function oklabToXyz(lab: Vec3): Vec3;
13
+ /** A lemtone color's light as XYZ with Y = 1 (one lumen's worth, to add up), or null where there's no real color. */
14
+ export declare function abToXyz(a: number, b: number): Vec3 | null;
15
+ /** Light's color as a lemtone's a and b (its brightness doesn't matter). */
16
+ export declare function xyzToAb(X: Vec3): [number, number];
17
+ /** A chromaticity's OKLab a, b at L = 1 (OKLab scales with XYZ's cube root, so each chromaticity is one direction). */
18
+ export declare function abOfXy(xy: XY): [number, number];
19
+ /** The Planckian locus (Kim et al. cubic spline, 1667–25000 K), as the tool page has it. */
20
+ export declare function cctToXy(kelvin: number): XY;
21
+ /** A white of this color temperature, as a and b. */
22
+ export declare const whiteAB: (kelvin: number) => [number, number];
23
+ export declare const WHITE_K_MIN = 1700;
24
+ export declare const WHITE_K_MAX = 10000;
25
+ /**
26
+ * The color temperature of a white, or null for a color more than
27
+ * `tolerance` (in a/b) from the Planckian locus. Infinity finds the nearest
28
+ * white to any color.
29
+ */
30
+ export declare function kelvinOf(a: number, b: number, tolerance?: number): number | null;
31
+ /** Hue (degrees, 0–360) and chroma of a color: OKLCh at L = 1. */
32
+ export declare function hueChroma(a: number, b: number): {
33
+ hue: number;
34
+ chroma: number;
35
+ };
36
+ export declare function abOf(hue: number, chroma: number): [number, number];
37
+ //# sourceMappingURL=lemtone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lemtone.d.ts","sourceRoot":"","sources":["../src/lemtone.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC5C,MAAM,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAElC,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,eAAO,MAAM,IAAI,MAAO,MAAM,EAAE,EAAE,KAAK,IAAI,KAAG,IAI7C,CAAC;AAUF,wBAAgB,UAAU,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,CAExC;AAED,iDAAiD;AACjD,eAAO,MAAM,QAAQ,MAAO,IAAI,KAAG,IAAwB,CAAC;AAuB5D,iDAAiD;AACjD,eAAO,MAAM,QAAQ,QAAS,IAAI,KAAG,IAA0B,CAAC;AAEhE,uCAAuC;AACvC,wBAAgB,UAAU,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,CAG1C;AAED,qHAAqH;AACrH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAIzD;AAED,4EAA4E;AAC5E,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAGjD;AAID,uHAAuH;AACvH,wBAAgB,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/C;AAED,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,EAAE,CAiB1C;AAED,qDAAqD;AACrD,eAAO,MAAM,OAAO,WAAY,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,CAA4B,CAAC;AAErF,eAAO,MAAM,WAAW,OAAO,CAAC;AAChC,eAAO,MAAM,WAAW,QAAQ,CAAC;AAIjC;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,SAAkB,GAAG,MAAM,GAAG,IAAI,CAGzF;AA2CD,kEAAkE;AAClE,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAI/E;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAGlE"}
@@ -0,0 +1,158 @@
1
+ // Lemtone: device-independent light, as arclamp's color channels carry it.
2
+ // Walter's rc-bulb-firmware lemtone/ is canonical.
3
+ //
4
+ // A lemtone is a color as OKLab a and b at L = 1 (so color doesn't depend on
5
+ // brightness), a brightness where 1.0 is 1000 lm (a fixed unit, not any
6
+ // light's maximum), and a fidelity: 0 builds pale colors and whites from the
7
+ // RGB LEDs, 1 from the white ones. Each bulb solves this into its own PWM.
8
+ //
9
+ // This is the color-space core: OKLab, whites by color temperature, and hue
10
+ // and chroma. Sketches use it through color.ts; vibes uses it too
11
+ // (src/shared/lemtone.ts), so a sketch, the picker and the map agree.
12
+ /** What brightness 1.0 means (rclamp sink.LumensPerBrightness). */
13
+ export const LUMENS_PER_BRIGHTNESS = 1000;
14
+ export const mul3 = (m, v) => [
15
+ m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
16
+ m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
17
+ m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
18
+ ];
19
+ // XYZ -> LMS and the LMS' -> OKLab step, as in Walter's Lemtone tool page
20
+ // (Björn Ottosson's OKLab).
21
+ const XYZ_LMS = [
22
+ [0.8189330101, 0.3618667424, -0.1288597137],
23
+ [0.0329845436, 0.9293118715, 0.0361456387],
24
+ [0.0482003018, 0.2643662691, 0.633851707],
25
+ ];
26
+ export function xyzToOklab(X) {
27
+ return lmsToOklab(xyzToLms(X));
28
+ }
29
+ /** XYZ as OKLab's LMS (before the cube root). */
30
+ export const xyzToLms = (X) => mul3(XYZ_LMS, X);
31
+ function lmsToOklab(lms) {
32
+ const [l, m, s] = lms.map(Math.cbrt);
33
+ return [
34
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
35
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
36
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
37
+ ];
38
+ }
39
+ // OKLab -> LMS' and LMS -> XYZ (Ottosson's inverses).
40
+ const LAB_LMS = [
41
+ [1.0, 0.3963377774, 0.2158037573],
42
+ [1.0, -0.1055613458, -0.0638541728],
43
+ [1.0, -0.0894841775, -1.291485548],
44
+ ];
45
+ const LMS_XYZ = [
46
+ [1.2270138511, -0.5577999807, 0.281256149],
47
+ [-0.0405801784, 1.1122568696, -0.0716766787],
48
+ [-0.0763812845, -0.4214819784, 1.5861632204],
49
+ ];
50
+ /** OKLab's LMS (before the cube root) as XYZ. */
51
+ export const lmsToXyz = (lms) => mul3(LMS_XYZ, lms);
52
+ /** An OKLab color (L, a, b) as XYZ. */
53
+ export function oklabToXyz(lab) {
54
+ const [l, m, s] = mul3(LAB_LMS, lab);
55
+ return lmsToXyz([l * l * l, m * m * m, s * s * s]);
56
+ }
57
+ /** A lemtone color's light as XYZ with Y = 1 (one lumen's worth, to add up), or null where there's no real color. */
58
+ export function abToXyz(a, b) {
59
+ const X = oklabToXyz([1, a, b]);
60
+ if (!(X[1] > 1e-6))
61
+ return null;
62
+ return [X[0] / X[1], 1, X[2] / X[1]];
63
+ }
64
+ /** Light's color as a lemtone's a and b (its brightness doesn't matter). */
65
+ export function xyzToAb(X) {
66
+ const [L, a, b] = xyzToOklab(X);
67
+ return L > 0 ? [a / L, b / L] : [0, 0];
68
+ }
69
+ const xyToXyz = ([x, y]) => [x / y, 1, (1 - x - y) / y];
70
+ /** A chromaticity's OKLab a, b at L = 1 (OKLab scales with XYZ's cube root, so each chromaticity is one direction). */
71
+ export function abOfXy(xy) {
72
+ const [L, a, b] = xyzToOklab(xyToXyz(xy));
73
+ return [a / L, b / L];
74
+ }
75
+ /** The Planckian locus (Kim et al. cubic spline, 1667–25000 K), as the tool page has it. */
76
+ export function cctToXy(kelvin) {
77
+ const t = 1e3 / kelvin;
78
+ const t2 = t * t;
79
+ const t3 = t2 * t;
80
+ const x = kelvin <= 4000
81
+ ? -0.2661239 * t3 - 0.2343589 * t2 + 0.8776956 * t + 0.17991
82
+ : -3.0258469 * t3 + 2.1070379 * t2 + 0.2226347 * t + 0.24039;
83
+ const x2 = x * x;
84
+ const x3 = x2 * x;
85
+ const y = kelvin <= 2222
86
+ ? -1.1063814 * x3 - 1.3481102 * x2 + 2.18555832 * x - 0.20219683
87
+ : kelvin <= 4000
88
+ ? -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867
89
+ : 3.081758 * x3 - 5.8733867 * x2 + 3.75112997 * x - 0.37001483;
90
+ return [x, y];
91
+ }
92
+ /** A white of this color temperature, as a and b. */
93
+ export const whiteAB = (kelvin) => abOfXy(cctToXy(kelvin));
94
+ export const WHITE_K_MIN = 1700;
95
+ export const WHITE_K_MAX = 10000;
96
+ // How far from the Planckian locus (in a/b) a color still counts as a white.
97
+ const WHITE_TOLERANCE = 0.005;
98
+ /**
99
+ * The color temperature of a white, or null for a color more than
100
+ * `tolerance` (in a/b) from the Planckian locus. Infinity finds the nearest
101
+ * white to any color.
102
+ */
103
+ export function kelvinOf(a, b, tolerance = WHITE_TOLERANCE) {
104
+ const { mired, off } = nearestWhite(a, b);
105
+ return off <= tolerance ? 1e6 / mired : null;
106
+ }
107
+ // The coarse pass's whites, every 2 mired across the range, worked out once.
108
+ let coarse = null;
109
+ // Nearest whites found already: the same few colors are named over and over as things render.
110
+ const nearest = new Map();
111
+ /** The nearest white to a color (in mired) and how far off it the color is (in a/b). */
112
+ function nearestWhite(a, b) {
113
+ const key = `${a} ${b}`;
114
+ const known = nearest.get(key);
115
+ if (known)
116
+ return known;
117
+ if (nearest.size > 4096)
118
+ nearest.clear();
119
+ if (!coarse) {
120
+ coarse = [];
121
+ for (let m = 1e6 / WHITE_K_MAX; m <= 1e6 / WHITE_K_MIN; m += 2) {
122
+ const [wa, wb] = whiteAB(1e6 / m);
123
+ coarse.push({ m, a: wa, b: wb });
124
+ }
125
+ }
126
+ // Coarse to fine, in mired: even steps in mired are even steps in look.
127
+ let best = 0;
128
+ let bestD = Infinity;
129
+ for (const w of coarse) {
130
+ const d = Math.hypot(w.a - a, w.b - b);
131
+ if (d < bestD) {
132
+ bestD = d;
133
+ best = w.m;
134
+ }
135
+ }
136
+ for (let m = best - 2; m <= best + 2; m += 0.02) {
137
+ const [wa, wb] = whiteAB(1e6 / m);
138
+ const d = Math.hypot(wa - a, wb - b);
139
+ if (d < bestD) {
140
+ bestD = d;
141
+ best = m;
142
+ }
143
+ }
144
+ const found = { mired: best, off: bestD };
145
+ nearest.set(key, found);
146
+ return found;
147
+ }
148
+ /** Hue (degrees, 0–360) and chroma of a color: OKLCh at L = 1. */
149
+ export function hueChroma(a, b) {
150
+ let hue = (Math.atan2(b, a) * 180) / Math.PI;
151
+ if (hue < 0)
152
+ hue += 360;
153
+ return { hue, chroma: Math.hypot(a, b) };
154
+ }
155
+ export function abOf(hue, chroma) {
156
+ const r = (hue * Math.PI) / 180;
157
+ return [chroma * Math.cos(r), chroma * Math.sin(r)];
158
+ }
@@ -0,0 +1,5 @@
1
+ /** A CSS named color's hex digits ("ffa500" for orange), or undefined. */
2
+ export declare function cssNamed(name: string): string | undefined;
3
+ /** Whites by what they're the light of, in kelvin. */
4
+ export declare const LIGHT_NAMES: Readonly<Record<string, number>>;
5
+ //# sourceMappingURL=names.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../src/names.ts"],"names":[],"mappings":"AAiCA,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOzD;AAED,sDAAsD;AACtD,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKxD,CAAC"}
package/dist/names.js ADDED
@@ -0,0 +1,46 @@
1
+ // Colors by name: CSS's named colors (as sRGB hex), and a few color
2
+ // temperatures by what they're the light of.
3
+ const CSS = "aliceblue f0f8ff antiquewhite faebd7 aqua 00ffff aquamarine 7fffd4 azure f0ffff beige f5f5dc bisque ffe4c4 " +
4
+ "black 000000 blanchedalmond ffebcd blue 0000ff blueviolet 8a2be2 brown a52a2a burlywood deb887 " +
5
+ "cadetblue 5f9ea0 chartreuse 7fff00 chocolate d2691e coral ff7f50 cornflowerblue 6495ed cornsilk fff8dc " +
6
+ "crimson dc143c cyan 00ffff darkblue 00008b darkcyan 008b8b darkgoldenrod b8860b darkgray a9a9a9 " +
7
+ "darkgreen 006400 darkgrey a9a9a9 darkkhaki bdb76b darkmagenta 8b008b darkolivegreen 556b2f " +
8
+ "darkorange ff8c00 darkorchid 9932cc darkred 8b0000 darksalmon e9967a darkseagreen 8fbc8f " +
9
+ "darkslateblue 483d8b darkslategray 2f4f4f darkslategrey 2f4f4f darkturquoise 00ced1 darkviolet 9400d3 " +
10
+ "deeppink ff1493 deepskyblue 00bfff dimgray 696969 dimgrey 696969 dodgerblue 1e90ff firebrick b22222 " +
11
+ "floralwhite fffaf0 forestgreen 228b22 fuchsia ff00ff gainsboro dcdcdc ghostwhite f8f8ff gold ffd700 " +
12
+ "goldenrod daa520 gray 808080 green 008000 greenyellow adff2f grey 808080 honeydew f0fff0 hotpink ff69b4 " +
13
+ "indianred cd5c5c indigo 4b0082 ivory fffff0 khaki f0e68c lavender e6e6fa lavenderblush fff0f5 " +
14
+ "lawngreen 7cfc00 lemonchiffon fffacd lightblue add8e6 lightcoral f08080 lightcyan e0ffff " +
15
+ "lightgoldenrodyellow fafad2 lightgray d3d3d3 lightgreen 90ee90 lightgrey d3d3d3 lightpink ffb6c1 " +
16
+ "lightsalmon ffa07a lightseagreen 20b2aa lightskyblue 87cefa lightslategray 778899 lightslategrey 778899 " +
17
+ "lightsteelblue b0c4de lightyellow ffffe0 lime 00ff00 limegreen 32cd32 linen faf0e6 magenta ff00ff " +
18
+ "maroon 800000 mediumaquamarine 66cdaa mediumblue 0000cd mediumorchid ba55d3 mediumpurple 9370db " +
19
+ "mediumseagreen 3cb371 mediumslateblue 7b68ee mediumspringgreen 00fa9a mediumturquoise 48d1cc " +
20
+ "mediumvioletred c71585 midnightblue 191970 mintcream f5fffa mistyrose ffe4e1 moccasin ffe4b5 " +
21
+ "navajowhite ffdead navy 000080 oldlace fdf5e6 olive 808000 olivedrab 6b8e23 orange ffa500 " +
22
+ "orangered ff4500 orchid da70d6 palegoldenrod eee8aa palegreen 98fb98 paleturquoise afeeee " +
23
+ "palevioletred db7093 papayawhip ffefd5 peachpuff ffdab9 peru cd853f pink ffc0cb plum dda0dd " +
24
+ "powderblue b0e0e6 purple 800080 rebeccapurple 663399 red ff0000 rosybrown bc8f8f royalblue 4169e1 " +
25
+ "saddlebrown 8b4513 salmon fa8072 sandybrown f4a460 seagreen 2e8b57 seashell fff5ee sienna a0522d " +
26
+ "silver c0c0c0 skyblue 87ceeb slateblue 6a5acd slategray 708090 slategrey 708090 snow fffafa " +
27
+ "springgreen 00ff7f steelblue 4682b4 tan d2b48c teal 008080 thistle d8bfd8 tomato ff6347 " +
28
+ "turquoise 40e0d0 violet ee82ee wheat f5deb3 white ffffff whitesmoke f5f5f5 yellow ffff00 yellowgreen 9acd32";
29
+ let named = null;
30
+ /** A CSS named color's hex digits ("ffa500" for orange), or undefined. */
31
+ export function cssNamed(name) {
32
+ if (!named) {
33
+ named = new Map();
34
+ const words = CSS.split(" ");
35
+ for (let i = 0; i < words.length; i += 2)
36
+ named.set(words[i], words[i + 1]);
37
+ }
38
+ return named.get(name);
39
+ }
40
+ /** Whites by what they're the light of, in kelvin. */
41
+ export const LIGHT_NAMES = {
42
+ candlelight: 1850,
43
+ incandescent: 2700,
44
+ halogen: 3000,
45
+ daylight: 6500,
46
+ };
@@ -0,0 +1,43 @@
1
+ import type { Vec3 } from "./lemtone.ts";
2
+ /** What some text says a color is. */
3
+ export type Spec = {
4
+ alpha?: number;
5
+ lm?: number;
6
+ fidelity?: number;
7
+ } & ({
8
+ space: "off";
9
+ } | {
10
+ space: "srgb" | "srgb-linear" | "display-p3";
11
+ rgb: Vec3;
12
+ } | {
13
+ space: "xyz" | "xyz-d50";
14
+ xyz: Vec3;
15
+ } | {
16
+ space: "hsl";
17
+ h: number;
18
+ s: number;
19
+ l: number;
20
+ } | {
21
+ space: "hwb";
22
+ h: number;
23
+ w: number;
24
+ b: number;
25
+ } | {
26
+ space: "oklab" | "lab";
27
+ lab: Vec3;
28
+ } | {
29
+ space: "oklch" | "lch";
30
+ l: number;
31
+ c: number;
32
+ h: number;
33
+ } | {
34
+ space: "kelvin";
35
+ kelvin: number;
36
+ } | {
37
+ space: "lemtone";
38
+ hue: number;
39
+ chroma: number;
40
+ });
41
+ /** What `text` says a color is, or null if it can't be read. */
42
+ export declare function parseColor(text: string): Spec | null;
43
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAGzC,sCAAsC;AACtC,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,CACpE;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,GAChB;IAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,YAAY,CAAC;IAAC,GAAG,EAAE,IAAI,CAAA;CAAE,GAC3D;IAAE,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IAAC,GAAG,EAAE,IAAI,CAAA;CAAE,GACvC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,GAAG,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CACpD,CAAC;AAiCF,gEAAgE;AAChE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAqBpD"}