@audiofool/moondog 0.1.0 → 0.2.0-beta.3
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 +202 -0
- package/README.md +156 -68
- package/contracts/v1/definitions.schema.json +1 -169
- package/package.json +7 -23
- package/scripts/contract-lib.mjs +0 -7
- package/scripts/contract-semantics.mjs +0 -1904
- package/scripts/moondog.mjs +285 -74
- package/src/core/apple-library-source-status.mjs +0 -16
- package/src/core/apple-projection-domain-services.mjs +253 -53
- package/src/core/application-update.mjs +320 -0
- package/src/core/capability-catalog.mjs +290 -55
- package/src/core/listener-avoids.mjs +101 -0
- package/src/core/listening-now.mjs +108 -0
- package/src/core/listening-profile-domain-services.mjs +189 -41
- package/src/core/listening-upkeep.mjs +31 -0
- package/src/core/moondog-application.mjs +2115 -107
- package/src/core/playback-preferences.mjs +97 -0
- package/src/core/spotify-listening-intent.mjs +124 -0
- package/src/core/synthetic-domain-services.mjs +24 -3
- package/src/core/track-title.mjs +107 -0
- package/src/demo/moondog-demo.mjs +4 -22
- package/src/i18n/index.mjs +285 -0
- package/src/i18n/messages/de.mjs +135 -0
- package/src/i18n/messages/en.mjs +136 -0
- package/src/i18n/messages/es.mjs +135 -0
- package/src/i18n/messages/fr.mjs +135 -0
- package/src/i18n/messages/it.mjs +135 -0
- package/src/i18n/messages/ja.mjs +135 -0
- package/src/i18n/messages/ko.mjs +135 -0
- package/src/i18n/messages/pt.mjs +135 -0
- package/src/i18n/messages/zh-Hant.mjs +135 -0
- package/src/i18n/messages/zh.mjs +135 -0
- package/src/i18n/preferences.mjs +34 -0
- package/src/i18n/screen/es.mjs +967 -0
- package/src/i18n/screen/ja.mjs +967 -0
- package/src/i18n/screen/pt.mjs +967 -0
- package/src/i18n/screen/zh.mjs +967 -0
- package/src/importers/apple-music-library/index.mjs +0 -16
- package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
- package/src/integrations/apple-music/catalog.mjs +166 -14
- package/src/integrations/apple-music/music-app.mjs +232 -0
- package/src/integrations/apple-music/storefront.mjs +28 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
- package/src/integrations/lastfm/history.mjs +221 -0
- package/src/integrations/lastfm/settings.mjs +75 -0
- package/src/integrations/netease/ncm-cli-player.mjs +328 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
- package/src/integrations/public-playlists.mjs +2 -1
- package/src/integrations/spotify/account-data-profile.mjs +14 -6
- package/src/integrations/spotify/authentication.mjs +149 -15
- package/src/integrations/spotify/catalog-resolver.mjs +9 -6
- package/src/integrations/spotify/connection.mjs +2 -1
- package/src/integrations/spotify/credential-store.mjs +4 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
- package/src/integrations/spotify/history-archive.mjs +1 -1
- package/src/integrations/spotify/library-snapshot.mjs +227 -0
- package/src/integrations/spotify/recent-activity.mjs +2 -0
- package/src/integrations/spotify/service.mjs +873 -69
- package/src/integrations/spotify/web-api-client.mjs +410 -127
- package/src/integrations/web/brave-web.mjs +45 -0
- package/src/integrations/web/codex-web.mjs +15 -82
- package/src/integrations/web/deepseek-web.mjs +81 -0
- package/src/integrations/web/web-research.mjs +119 -0
- package/src/integrations/web/web-result.mjs +215 -0
- package/src/integrations/youtube-music/takeout.mjs +3 -1
- package/src/memory/local-memory-store.mjs +542 -57
- package/src/profile/history-import.mjs +42 -6
- package/src/profile/listener-corrections.mjs +22 -0
- package/src/profile/listener-profile-build.mjs +461 -0
- package/src/profile/listener-profile-store.mjs +130 -0
- package/src/profile/listener-profile-verification.mjs +68 -0
- package/src/profile/listener-profile-worker.mjs +179 -0
- package/src/profile/listening-history-store.mjs +640 -17
- package/src/profile/listening-profile-projection.mjs +454 -86
- package/src/profile/local-music-data.mjs +1 -1
- package/src/profile/moondog-suggestions.mjs +184 -0
- package/src/profile/music-providers.mjs +11 -7
- package/src/profile/profile-exploration.mjs +92 -0
- package/src/profile/recorded-measures.mjs +24 -0
- package/src/profile/spotify-archive-taste.mjs +0 -4
- package/src/runtime/pi/agent-runtime.mjs +2536 -692
- package/src/runtime/pi/authentication.mjs +34 -1
- package/src/runtime/pi/configured-runtime.mjs +22 -6
- package/src/runtime/pi/conversation-name.mjs +53 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
- package/src/runtime/pi/model-catalog.mjs +28 -5
- package/src/runtime/pi/model-settings.mjs +202 -0
- package/src/runtime/pi/model-transport.mjs +10 -0
- package/src/runtime/pi/persistent-credential-store.mjs +67 -31
- package/src/runtime/pi/profile-build-runtime.mjs +161 -0
- package/src/runtime/pi/public-model-catalog.mjs +288 -0
- package/src/runtime/pi/runtime-settings.mjs +10 -2
- package/src/runtime/pi/skills/listener-profile/SKILL.md +48 -0
- package/src/runtime/pi/tool-trace.mjs +37 -0
- package/src/surfaces/cli/auth-command.mjs +12 -4
- package/src/surfaces/cli/brand-components.mjs +310 -132
- package/src/surfaces/cli/brand-theme.mjs +51 -28
- package/src/surfaces/cli/command-completions.mjs +58 -42
- package/src/surfaces/cli/format-output.mjs +199 -388
- package/src/surfaces/cli/history-import-view.mjs +292 -139
- package/src/surfaces/cli/import-guides.mjs +2 -0
- package/src/surfaces/cli/listener-profile-command.mjs +92 -0
- package/src/surfaces/cli/listening-menu.mjs +7 -4
- package/src/surfaces/cli/profile-command.mjs +30 -21
- package/src/surfaces/cli/seasons-chart.mjs +87 -0
- package/src/surfaces/cli/spotify-command.mjs +54 -6
- package/src/surfaces/cli/taste-profile-model.mjs +78 -79
- package/src/surfaces/cli/taste-profile-view.mjs +33 -26
- package/src/surfaces/cli/terminal-art.mjs +140 -35
- package/src/surfaces/cli/tui.mjs +1066 -357
- package/src/surfaces/cli/update-command.mjs +32 -0
- package/src/surfaces/cli/web-command.mjs +53 -14
- package/src/surfaces/html/tasteprint.mjs +43 -70
- package/contracts/v1/capability-policy.schema.json +0 -572
- package/contracts/v1/profile-evidence.schema.json +0 -237
- package/contracts/v1/tool-invocation.schema.json +0 -318
- package/contracts/v1/tool-receipt.schema.json +0 -1066
- package/scripts/demo-studio.mjs +0 -58
- package/src/core/apple-data-migration.mjs +0 -50
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
- package/src/demo/moondog-tasteprint-demo.mjs +0 -631
- package/src/integrations/spotify/index.mjs +0 -80
- package/src/surfaces/web/showcase.mjs +0 -272
- package/src/surfaces/web/studio-page.mjs +0 -4148
- package/src/surfaces/web/studio.mjs +0 -2288
|
@@ -3,7 +3,15 @@ export const LOGO_MOTION_INTERVAL_MS = 125;
|
|
|
3
3
|
|
|
4
4
|
const turn = Math.PI * 2;
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Record layers from faintest to strongest, matching the logo's fine grooves and solid dog. */
|
|
7
|
+
export const RECORD_LAYERS = Object.freeze({ groove: 1, surface: 2, figure: 3 });
|
|
8
|
+
|
|
9
|
+
// The dog's eye, in the record's unit coordinates; layouts line the title up with it.
|
|
10
|
+
const eye = [0.22, -0.13];
|
|
11
|
+
|
|
12
|
+
// The dog's outline runs through these points as one smooth curve (smoothOutline, below), so it
|
|
13
|
+
// keeps its shape without the facets of straight segments between them.
|
|
14
|
+
const dogPoints = [
|
|
7
15
|
[-0.62, 0.48], [-0.55, 0.29], [-0.47, 0.08], [-0.42, -0.16],
|
|
8
16
|
[-0.4, -0.34], [-0.32, -0.47], [-0.18, -0.61], [-0.17, -0.44],
|
|
9
17
|
[-0.12, -0.43], [-0.1, -0.32], [0.02, -0.27], [0.16, -0.24],
|
|
@@ -13,6 +21,26 @@ const dog = [
|
|
|
13
21
|
[0.07, 0.77], [-0.13, 0.75], [-0.3, 0.7], [-0.48, 0.62],
|
|
14
22
|
];
|
|
15
23
|
|
|
24
|
+
// A closed centripetal Catmull-Rom curve through `points`: it passes through every point, and unlike
|
|
25
|
+
// the uniform kind it never loops or overshoots at the sharp turns of the ear and the nose.
|
|
26
|
+
function smoothOutline(points, steps = 10) {
|
|
27
|
+
const outline = [];
|
|
28
|
+
const count = points.length;
|
|
29
|
+
for (let index = 0; index < count; index += 1) {
|
|
30
|
+
const p = [-1, 0, 1, 2].map((offset) => points[(index + offset + count) % count]);
|
|
31
|
+
const t = [0];
|
|
32
|
+
for (let at = 1; at < 4; at += 1) t.push(t[at - 1] + Math.hypot(p[at][0] - p[at - 1][0], p[at][1] - p[at - 1][1]) ** 0.5);
|
|
33
|
+
for (let step = 0; step < steps; step += 1) {
|
|
34
|
+
const u = t[1] + (t[2] - t[1]) * step / steps;
|
|
35
|
+
const mix = (a, b, from, to) => a.map((value, axis) => ((to - u) * value + (u - from) * b[axis]) / (to - from));
|
|
36
|
+
const [a1, a2, a3] = [mix(p[0], p[1], t[0], t[1]), mix(p[1], p[2], t[1], t[2]), mix(p[2], p[3], t[2], t[3])];
|
|
37
|
+
outline.push(mix(mix(a1, a2, t[0], t[2]), mix(a2, a3, t[1], t[3]), t[1], t[2]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return outline;
|
|
41
|
+
}
|
|
42
|
+
const dog = smoothOutline(dogPoints);
|
|
43
|
+
|
|
16
44
|
const smallAscii = [
|
|
17
45
|
" /",
|
|
18
46
|
" .-======-. /",
|
|
@@ -94,21 +122,25 @@ function bezier(start, first, second, end) {
|
|
|
94
122
|
});
|
|
95
123
|
}
|
|
96
124
|
|
|
97
|
-
function drawing(columns, rows) {
|
|
125
|
+
function drawing(columns, rows, cellAspect) {
|
|
98
126
|
const width = columns * 2;
|
|
99
127
|
const height = rows * 4;
|
|
128
|
+
// Each dot remembers which layer inked it, so callers can tone the record like the logo.
|
|
100
129
|
const dots = new Uint8Array(width * height);
|
|
101
|
-
|
|
130
|
+
let layer = RECORD_LAYERS.figure;
|
|
131
|
+
// A Braille dot is as tall as a quarter cell and as wide as half of one.
|
|
132
|
+
const stretch = cellAspect / 2;
|
|
133
|
+
const radius = Math.min((width - 4) / 2.35, (height - 4) * stretch / 2.24);
|
|
102
134
|
const centerX = (width - 1) / 2 - radius * 0.09;
|
|
103
|
-
const centerY = (height - 1) / 2 + radius * 0.045;
|
|
135
|
+
const centerY = (height - 1) / 2 + radius * 0.045 / stretch;
|
|
104
136
|
const pixel = 1 / radius;
|
|
105
137
|
|
|
106
138
|
function scan(left, top, right, bottom, predicate, ink) {
|
|
107
|
-
for (let y = Math.max(0, Math.floor(centerY + top * radius)); y <= Math.min(height - 1, Math.ceil(centerY + bottom * radius)); y += 1) {
|
|
139
|
+
for (let y = Math.max(0, Math.floor(centerY + top * radius / stretch)); y <= Math.min(height - 1, Math.ceil(centerY + bottom * radius / stretch)); y += 1) {
|
|
108
140
|
for (let x = Math.max(0, Math.floor(centerX + left * radius)); x <= Math.min(width - 1, Math.ceil(centerX + right * radius)); x += 1) {
|
|
109
141
|
const nx = (x - centerX) / radius;
|
|
110
|
-
const ny = (y - centerY) / radius;
|
|
111
|
-
if (predicate(nx, ny, x, y)) dots[y * width + x] = ink ?
|
|
142
|
+
const ny = (y - centerY) * stretch / radius;
|
|
143
|
+
if (predicate(nx, ny, x, y)) dots[y * width + x] = ink ? layer : 0;
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
}
|
|
@@ -142,21 +174,43 @@ function drawing(columns, rows) {
|
|
|
142
174
|
const ys = points.map(([, y]) => y);
|
|
143
175
|
scan(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys),
|
|
144
176
|
(x, y, dx, dy) => insidePolygon(x, y, points)
|
|
145
|
-
|
|
177
|
+
// The hatch runs diagonally, two dots on and two off, like an engraving's.
|
|
178
|
+
&& (!hatch || (dx + dy) % 4 < 2), ink);
|
|
146
179
|
}
|
|
147
180
|
|
|
148
|
-
|
|
181
|
+
const paintLayer = (value) => { layer = value; };
|
|
182
|
+
return { dots, width, height, radius, centerX, centerY, pixel, circle, disc, line, polygon, paintLayer };
|
|
149
183
|
}
|
|
150
184
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
185
|
+
// The portrait and the tonearm in the record's unit coordinates: the disc has radius 1 around 0,0.
|
|
186
|
+
const shapes = {
|
|
187
|
+
headband: bezier([-0.42, -0.03], [-0.47, -0.31], [-0.27, -0.58], [-0.18, -0.59]),
|
|
188
|
+
ear: [-0.365, -0.005],
|
|
189
|
+
nose: [0.53, 0.045],
|
|
190
|
+
arm: [[1.17, -1.095], [0.91, -0.8]],
|
|
191
|
+
cartridge: [[0.92, -0.845], [0.998, -0.777], [0.808, -0.567], [0.73, -0.635]],
|
|
192
|
+
// Two short highlights ride fixed grooves as the record turns: the groove's place from the rim, its angle, its length.
|
|
193
|
+
highlights: [[1, -0.91, 0.12], [3, 2.44, 0.09]],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The shapes the record is drawn from, for surfaces that draw it as vectors, such as the window's small mark. */
|
|
197
|
+
export function lunarRecordShapes() {
|
|
198
|
+
return structuredClone({ dog, eye, ...shapes, earRadius: 0.17, grooves: [1, 0.91, 0.8, 0.66] });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The record as a field of dots, two across and four down per terminal cell, each marked with its layer.
|
|
202
|
+
function paint(columns, rows, phase, cellAspect) {
|
|
203
|
+
const art = drawing(columns, rows, cellAspect);
|
|
204
|
+
const { radius, pixel, circle, disc, line, polygon, paintLayer } = art;
|
|
154
205
|
const detail = radius >= 39 ? 2 : radius >= 27 ? 1 : 0;
|
|
155
206
|
const grooves = detail === 2
|
|
156
207
|
? [1, 0.975, 0.935, 0.895, 0.85, 0.8, 0.74, 0.67, 0.59, 0.51]
|
|
157
208
|
: detail === 1 ? [1, 0.955, 0.89, 0.82, 0.74, 0.64, 0.53]
|
|
158
209
|
: [1, 0.91, 0.8, 0.66];
|
|
159
|
-
|
|
210
|
+
paintLayer(RECORD_LAYERS.groove);
|
|
211
|
+
// A full dot keeps each ring continuous through its diagonals without merging neighbours.
|
|
212
|
+
for (const groove of grooves) circle(0, 0, groove, pixel);
|
|
213
|
+
paintLayer(RECORD_LAYERS.surface);
|
|
160
214
|
|
|
161
215
|
const rotation = phase / LOGO_MOTION_FRAMES * turn;
|
|
162
216
|
const cosine = Math.cos(rotation);
|
|
@@ -176,7 +230,8 @@ function braille(columns, rows, phase) {
|
|
|
176
230
|
|
|
177
231
|
// These short highlights follow fixed grooves; the record geometry never deforms.
|
|
178
232
|
// Painting the portrait afterward keeps the moving surface behind the dog and headphones.
|
|
179
|
-
for (const [
|
|
233
|
+
for (const [index, offset, length] of shapes.highlights) {
|
|
234
|
+
const groove = grooves[index];
|
|
180
235
|
const angle = rotation + offset;
|
|
181
236
|
const points = Array.from({ length: 13 }, (_, index) => {
|
|
182
237
|
const position = angle - length + index / 12 * length;
|
|
@@ -186,15 +241,18 @@ function braille(columns, rows, phase) {
|
|
|
186
241
|
disc(Math.cos(angle) * groove, Math.sin(angle) * groove, pixel * 1.1);
|
|
187
242
|
}
|
|
188
243
|
|
|
244
|
+
paintLayer(RECORD_LAYERS.figure);
|
|
245
|
+
// Like the logo, a clear halo separates the dog from the grooves around it.
|
|
246
|
+
const { headband, ear, nose, arm, cartridge } = shapes;
|
|
247
|
+
line([...dog, dog[0]], pixel * 4.5, false);
|
|
248
|
+
line(headband, Math.max(pixel * 7, 0.11), false);
|
|
189
249
|
polygon(dog, false);
|
|
190
250
|
polygon(dog, true, true);
|
|
191
251
|
line([...dog, dog[0]], pixel * 0.8);
|
|
192
252
|
|
|
193
|
-
const headband = bezier([-0.42, -0.03], [-0.47, -0.31], [-0.27, -0.58], [-0.18, -0.59]);
|
|
194
253
|
line(headband, Math.max(pixel * 3.5, 0.065));
|
|
195
254
|
line(headband, Math.max(pixel * 1.2, 0.025), false);
|
|
196
|
-
const earX =
|
|
197
|
-
const earY = -0.005;
|
|
255
|
+
const [earX, earY] = ear;
|
|
198
256
|
const earRadius = detail > 0 ? 0.157 : 0.17;
|
|
199
257
|
disc(earX, earY, earRadius + pixel * 1.6, false);
|
|
200
258
|
circle(earX, earY, earRadius, pixel * 1.1);
|
|
@@ -202,11 +260,10 @@ function braille(columns, rows, phase) {
|
|
|
202
260
|
if (detail > 0) circle(earX, earY, earRadius * 0.35, pixel);
|
|
203
261
|
else disc(earX, earY, pixel * 0.9);
|
|
204
262
|
// A small eye and nose keep the dog readable before the hatch pattern resolves.
|
|
205
|
-
disc(0
|
|
206
|
-
disc(0
|
|
263
|
+
disc(eye[0], eye[1], pixel * 1.05, false);
|
|
264
|
+
disc(nose[0], nose[1], pixel * 0.85);
|
|
207
265
|
|
|
208
|
-
line(
|
|
209
|
-
const cartridge = [[0.92, -0.845], [0.998, -0.777], [0.808, -0.567], [0.73, -0.635]];
|
|
266
|
+
line(arm, Math.max(pixel * 1.8, 0.03));
|
|
210
267
|
polygon(cartridge, true);
|
|
211
268
|
line([...cartridge, cartridge[0]], pixel);
|
|
212
269
|
disc(0.914, -0.764, Math.max(pixel * 0.7, 0.021), false);
|
|
@@ -216,16 +273,36 @@ function braille(columns, rows, phase) {
|
|
|
216
273
|
const contact = [Math.cos(angle) * 0.928, Math.sin(angle) * 0.928];
|
|
217
274
|
disc(contact[0], contact[1], pixel * (0.95 + pulse * 0.15));
|
|
218
275
|
|
|
276
|
+
return art;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function braille(columns, rows, phase, cellAspect) {
|
|
280
|
+
const art = paint(columns, rows, phase, cellAspect);
|
|
281
|
+
const { radius } = art;
|
|
219
282
|
const bit = [[1, 8], [2, 16], [4, 32], [64, 128]];
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
const tones = [];
|
|
284
|
+
const lines = Array.from({ length: rows }, (_, row) => {
|
|
285
|
+
const rowTones = [];
|
|
286
|
+
const line = Array.from({ length: columns }, (_, column) => {
|
|
287
|
+
let mask = 0;
|
|
288
|
+
let tone = 0;
|
|
289
|
+
for (let y = 0; y < 4; y += 1) {
|
|
290
|
+
for (let x = 0; x < 2; x += 1) {
|
|
291
|
+
const value = art.dots[(row * 4 + y) * art.width + column * 2 + x];
|
|
292
|
+
if (value) mask |= bit[y][x];
|
|
293
|
+
tone = Math.max(tone, value);
|
|
294
|
+
}
|
|
225
295
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
296
|
+
rowTones.push(tone);
|
|
297
|
+
return mask ? String.fromCodePoint(0x2800 + mask) : " ";
|
|
298
|
+
}).join("");
|
|
299
|
+
tones.push(rowTones);
|
|
300
|
+
return line;
|
|
301
|
+
});
|
|
302
|
+
// The disc's own center in cells; the tonearm reaches past it, so layouts align on this.
|
|
303
|
+
const stretch = cellAspect / 2;
|
|
304
|
+
const cell = (x, y) => [(art.centerX + x * radius + 0.5) / 2, (art.centerY + y * radius / stretch + 0.5) / 4];
|
|
305
|
+
return { lines, tones, center: cell(0, 0), eye: cell(...eye), radius: [radius / 2, radius / stretch / 4] };
|
|
229
306
|
}
|
|
230
307
|
|
|
231
308
|
function ascii(columns, rows, phase) {
|
|
@@ -252,27 +329,55 @@ function ascii(columns, rows, phase) {
|
|
|
252
329
|
if (animated[y]?.[x] !== undefined) animated[y][x] = mark;
|
|
253
330
|
}
|
|
254
331
|
}
|
|
255
|
-
|
|
332
|
+
const lines = Array.from({ length: rows }, (_, row) => {
|
|
256
333
|
const content = animated[row - top]?.join("") ?? "";
|
|
257
334
|
return (" ".repeat(left) + content).slice(0, columns).padEnd(columns);
|
|
258
335
|
});
|
|
336
|
+
const [centerX, centerY, radiusX, radiusY] = selected?.orbit ?? [width / 2, template.length / 2, width / 2, template.length / 2];
|
|
337
|
+
return {
|
|
338
|
+
lines,
|
|
339
|
+
center: [left + centerX + 0.5, top + centerY + 0.5],
|
|
340
|
+
eye: [left + centerX + eye[0] * radiusX + 0.5, top + centerY + eye[1] * radiusY + 0.5],
|
|
341
|
+
radius: [radiusX, radiusY],
|
|
342
|
+
};
|
|
259
343
|
}
|
|
260
344
|
|
|
261
345
|
/**
|
|
262
346
|
* Original terminal drawing, composed from geometry and hand-lettered ASCII.
|
|
263
|
-
* Braille uses a 2-by-4 dot grid
|
|
347
|
+
* Braille uses a 2-by-4 dot grid; cellAspect (cell height over width) keeps the record round,
|
|
348
|
+
* and the default 2 suits a typical 1:2 terminal cell.
|
|
264
349
|
* phase advances the moon surface and groove highlights through an eight-second loop.
|
|
265
350
|
* The portrait and cartridge stay fixed; scheduling and motion preferences belong to the caller.
|
|
351
|
+
* Every cell also gets a RECORD_LAYERS tone, so each layer can take its own ink.
|
|
266
352
|
*/
|
|
267
|
-
export function
|
|
353
|
+
export function renderLunarRecordLayers({ columns = 56, rows = 24, style = "braille", phase = 0, cellAspect = 2 } = {}) {
|
|
268
354
|
const width = Number.isFinite(columns) ? Math.max(0, Math.floor(columns)) : 56;
|
|
269
355
|
const height = Number.isFinite(rows) ? Math.max(0, Math.floor(rows)) : 24;
|
|
270
|
-
if (!width || !height)
|
|
356
|
+
if (!width || !height) {
|
|
357
|
+
const center = [width / 2, height / 2];
|
|
358
|
+
return { lines: Array.from({ length: height }, () => ""), tones: Array.from({ length: height }, () => []), center, eye: center, radius: center };
|
|
359
|
+
}
|
|
271
360
|
const frame = Number.isFinite(phase)
|
|
272
361
|
? ((Math.floor(phase) % LOGO_MOTION_FRAMES) + LOGO_MOTION_FRAMES) % LOGO_MOTION_FRAMES
|
|
273
362
|
: 0;
|
|
274
|
-
if (style === "ascii" || width < 20 || height < 9)
|
|
275
|
-
|
|
363
|
+
if (style === "ascii" || width < 20 || height < 9) {
|
|
364
|
+
// Hand-lettered ASCII has no separable layers; it keeps one ink.
|
|
365
|
+
const { lines, ...geometry } = ascii(width, height, frame);
|
|
366
|
+
return { lines, ...geometry, tones: lines.map((line) => Array.from(line, (character) => character === " " ? 0 : RECORD_LAYERS.figure)) };
|
|
367
|
+
}
|
|
368
|
+
const aspect = Number.isFinite(cellAspect) ? Math.min(3, Math.max(1.5, cellAspect)) : 2;
|
|
369
|
+
return braille(width, height, frame, aspect);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The same record as dots rather than Braille characters, for surfaces that draw their own dots,
|
|
374
|
+
* such as the desktop window's canvas. Dots sit on a square grid of width x height, row by row;
|
|
375
|
+
* each holds 0 or the RECORD_LAYERS value that inked it.
|
|
376
|
+
*/
|
|
377
|
+
export function renderLunarRecordDots({ columns = 56, rows = 28, phase = 0 } = {}) {
|
|
378
|
+
const frame = Number.isFinite(phase) ? ((Math.floor(phase) % LOGO_MOTION_FRAMES) + LOGO_MOTION_FRAMES) % LOGO_MOTION_FRAMES : 0;
|
|
379
|
+
const { dots, width, height } = paint(Math.max(10, Math.floor(columns)), Math.max(5, Math.floor(rows)), frame, 2);
|
|
380
|
+
return { dots, width, height };
|
|
276
381
|
}
|
|
277
382
|
|
|
278
383
|
/** Five-pixel letterforms, packed into three terminal rows without color or escapes. */
|