@roobscoob/vibes 0.0.0-stage → 0.2.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 +276 -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 +202 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +492 -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 +105 -0
  19. package/dist/here.d.ts.map +1 -0
  20. package/dist/here.js +219 -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 +29 -0
  40. package/dist/sketch.d.ts.map +1 -0
  41. package/dist/sketch.js +141 -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 +590 -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 +272 -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 +172 -0
  59. 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
+ }
@@ -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);
@@ -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
+ }
@@ -0,0 +1,29 @@
1
+ import { type ColorInput } from "./color.ts";
2
+ import type { Update } from "./arclamp.ts";
3
+ import { Here } from "./here.ts";
4
+ /** What a color 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
+ /** The color lights' look: a function of here giving a color, one color for every light, or a palette, laid across the room. */
7
+ export type Look = ((here: Here) => LookResult) | ColorInput;
8
+ /** Whether an old bulb should be on: true or false, a number (on at 0.5 or more), "on" or "off"; null or undefined leaves it as it is. */
9
+ export type SwitchResult = boolean | number | string | null | undefined | void;
10
+ /** The old bulbs' look: a function of here (a field too) saying whether each is on, or one answer for them all. */
11
+ export type SwitchLook = ((here: Here) => SwitchResult) | boolean | number | string;
12
+ /** A look for each kind of light (light.kind); either may be left out, and its lights are left as they are. */
13
+ export interface Looks {
14
+ /** For the color bulbs and panels. */
15
+ color?: Look;
16
+ /** For the old bulbs, which only switch. */
17
+ switch?: SwitchLook;
18
+ }
19
+ export interface SketchOptions {
20
+ /** The old bulbs' look, beside a color look given on its own. */
21
+ switch?: SwitchLook;
22
+ }
23
+ /** Your color look (a function of here, or a field) as arclamp's update(); old bulbs are left alone, unless given `switch`. */
24
+ export declare function sketch(look: (here: Here) => LookResult, options?: SketchOptions): Update;
25
+ /** A look for each kind of light: { color, switch }. */
26
+ export declare function sketch(looks: Looks): Update;
27
+ /** One color for every color light, or a palette laid across the room, as arclamp's update(). */
28
+ export declare function sketch(look: ColorInput, options?: SketchOptions): Update;
29
+ //# sourceMappingURL=sketch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sketch.d.ts","sourceRoot":"","sources":["../src/sketch.ts"],"names":[],"mappings":"AAuBA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAa,MAAM,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAW,IAAI,EAAiC,MAAM,WAAW,CAAC;AAKzE,uGAAuG;AACvG,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,GAAG,IAAI,CAAC;AAEtE,gIAAgI;AAChI,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,UAAU,CAAC,GAAG,UAAU,CAAC;AAE7D,0IAA0I;AAC1I,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;AAE/E,mHAAmH;AACnH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,YAAY,CAAC,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpF,+GAA+G;AAC/G,MAAM,WAAW,KAAK;IACpB,sCAAsC;IACtC,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,4CAA4C;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAKD,+HAA+H;AAC/H,wBAAgB,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC;AAC1F,wDAAwD;AACxD,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;AAC7C,iGAAiG;AACjG,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAAC"}
package/dist/sketch.js ADDED
@@ -0,0 +1,141 @@
1
+ // sketch(): your looks as arclamp's update().
2
+ //
3
+ // Color lights and old bulbs are controlled apart, each by a look of its own,
4
+ // named for the kind of light it's for (light.kind):
5
+ //
6
+ // sketch({ color: osc(3).color(palettes.dusk), switch: osc(1, 0.25).thresh() })
7
+ //
8
+ // The color look is asked about the color bulbs and panels only: a function
9
+ // of here (your own, or a field) giving a color, one color for every light,
10
+ // or a palette laid across the room. Anything that says a color will do (a
11
+ // Color, "#f80", "2700K", { r, g, b }, …). sketch(look) is
12
+ // sketch({ color: look }).
13
+ //
14
+ // The switch look is asked about the old bulbs only, which just switch: true or
15
+ // false, a number (on at 0.5 or more, as arclamp's own `on` channel), "on" or
16
+ // "off", or a function of here (a field too) giving one. Without one, the old
17
+ // bulbs are left as they are; a color never switches them.
18
+ //
19
+ // Null or undefined from either leaves the light as it is below (so does
20
+ // false from a color look). Something that can't be read leaves it as it is
21
+ // too, with a warning (once for each kind of mistake), so one slip doesn't
22
+ // stop the sketch.
23
+ import { Color, color, isPalette } from "./color.js";
24
+ import { colorOf, Here, roomOf, sameLayout } from "./here.js";
25
+ // A tick after a long gap (the sketch paused, or held up) counts as at most this long.
26
+ const MAX_DT = 0.25;
27
+ export function sketch(given, options = {}) {
28
+ if (options && typeof options === "object" && "on" in options)
29
+ throw new Error(ON_KEY);
30
+ const looks = isLooks(given) ? { switch: options.switch, ...given } : { color: given, switch: options.switch };
31
+ const askColor = looks.color === undefined ? null : lookOf(looks.color);
32
+ const askSwitch = looks.switch === undefined ? null : switchLookOf(looks.switch);
33
+ let room = null;
34
+ let last = null;
35
+ const warned = new Set();
36
+ const warn = (kind, text) => {
37
+ if (warned.has(kind))
38
+ return;
39
+ warned.add(kind);
40
+ console.warn(`${text} (warning once)`);
41
+ };
42
+ return (prev, nowMs, initialMs) => {
43
+ const list = Array.isArray(prev?.lights) ? prev.lights : [];
44
+ if (!room || !sameLayout(prev, room))
45
+ room = roomOf(prev, room);
46
+ const t = (nowMs - initialMs) / 1000;
47
+ const dt = last === null ? 0 : Math.min(MAX_DT, Math.max(0, (nowMs - last) / 1000));
48
+ last = nowMs;
49
+ const lights = [];
50
+ for (let i = 0; i < list.length; i++) {
51
+ const light = room.lights[i];
52
+ light.below = colorOf(list[i], light.kind);
53
+ light.belowOn = light.below.lm > 0;
54
+ const ask = light.kind === "switch" ? askSwitch : askColor;
55
+ if (!ask)
56
+ continue;
57
+ const v = ask(new Here(light.x, light.y, t, dt, nowMs, light, room));
58
+ if (light.kind === "switch") {
59
+ const on = readOn(v);
60
+ if (on === "unreadable") {
61
+ warn("on", `light ${light.id} is an old bulb, which only switches: its look gives true or false, a number (on at 0.5 or more), or "on" or "off", not ${describe(v)}; give old bulbs a look of their own, sketch({ color, switch })`);
62
+ }
63
+ else if (on !== null)
64
+ lights.push({ id: light.id, channels: { on: on ? 1 : 0 } });
65
+ continue;
66
+ }
67
+ if (v === null || v === undefined || v === false)
68
+ continue;
69
+ let c;
70
+ try {
71
+ c = v instanceof Color ? v : color(v);
72
+ }
73
+ catch (err) {
74
+ if (typeof v === "number") {
75
+ 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)`);
76
+ }
77
+ else {
78
+ warn("unreadable", `light ${light.id} was given something that isn't a color, so it's left as it is: ${err.message}`);
79
+ }
80
+ continue;
81
+ }
82
+ const channels = c.channels;
83
+ if (!Number.isFinite(channels.a) || !Number.isFinite(channels.b) || !Number.isFinite(channels.brightness) || !Number.isFinite(channels.fidelity)) {
84
+ warn("nan", `light ${light.id} was given a color that isn't all numbers, ${JSON.stringify(c)}, so it's left as it is`);
85
+ continue;
86
+ }
87
+ lights.push({ id: light.id, channels });
88
+ }
89
+ return { lights };
90
+ };
91
+ }
92
+ const ON_KEY = 'old bulbs\' look is `switch`, for the kind of light it\'s for: sketch({ color, switch }), or sketch(look, { switch })';
93
+ /** Whether a sketch was given a look for each kind of light ({ color, switch }) rather than one look. */
94
+ function isLooks(v) {
95
+ if (!v || typeof v !== "object" || v instanceof Color || Array.isArray(v))
96
+ return false;
97
+ const keys = Object.keys(v);
98
+ // { on } would read as an old bulb's channels, a color: say what's meant instead.
99
+ if (keys.includes("on"))
100
+ throw new Error(ON_KEY);
101
+ return keys.length > 0 && keys.every((k) => k === "color" || k === "switch");
102
+ }
103
+ /** The color look as a function of here. One color is the same everywhere (and said wrong, an error now, not each tick). */
104
+ function lookOf(look) {
105
+ if (isPalette(look))
106
+ return (here) => look(here.u);
107
+ if (typeof look === "function")
108
+ return look;
109
+ const c = color(look);
110
+ return () => c;
111
+ }
112
+ /** The switch look as a function of here. One answer is the same everywhere (and said wrong, an error now). */
113
+ function switchLookOf(look) {
114
+ if (typeof look === "function")
115
+ return look;
116
+ const on = readOn(look);
117
+ if (on === "unreadable")
118
+ throw new Error(`${describe(look)} isn't on or off: give true or false, a number (on at 0.5 or more), or "on" or "off"`);
119
+ return () => on;
120
+ }
121
+ /** On (true), off (false), leave it (null), or can't tell. */
122
+ function readOn(v) {
123
+ if (v === null || v === undefined)
124
+ return null;
125
+ if (typeof v === "boolean")
126
+ return v;
127
+ if (typeof v === "number")
128
+ return Number.isFinite(v) ? v >= 0.5 : "unreadable";
129
+ if (typeof v === "string") {
130
+ const s = v.trim().toLowerCase();
131
+ if (s === "on" || s === "true" || s === "yes")
132
+ return true;
133
+ if (s === "off" || s === "false" || s === "no")
134
+ return false;
135
+ return s !== "" && Number.isFinite(Number(s)) ? Number(s) >= 0.5 : "unreadable";
136
+ }
137
+ if (typeof v === "object" && !(v instanceof Color) && "on" in v)
138
+ return readOn(v.on);
139
+ return "unreadable";
140
+ }
141
+ const describe = (v) => (v instanceof Color ? `a color (${v})` : typeof v === "string" ? `"${v}"` : JSON.stringify(v) ?? String(v));
@@ -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"}