dsh-live-trace 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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +920 -0
  3. package/README.zh.md +790 -0
  4. package/assets/rain.ogg +0 -0
  5. package/bin/dsh-glyph-probe.js +51 -0
  6. package/bin/dsh-live-trace.js +29 -0
  7. package/bin/dsh-live-working.js +14 -0
  8. package/cordis.patch.yml +31 -0
  9. package/icon.svg +12 -0
  10. package/index.js +328 -0
  11. package/lib/client.js +178 -0
  12. package/lib/instance.js +68 -0
  13. package/lib/normalize.js +850 -0
  14. package/lib/paths.js +66 -0
  15. package/lib/protocol.js +115 -0
  16. package/lib/registry.js +232 -0
  17. package/lib/tools.js +257 -0
  18. package/lib/tracker.js +648 -0
  19. package/lib/transport.js +231 -0
  20. package/locale/en.json +6 -0
  21. package/locale/zh.json +6 -0
  22. package/package.json +94 -0
  23. package/picture/call1.png +0 -0
  24. package/picture/call2.png +0 -0
  25. package/picture/sleep1.png +0 -0
  26. package/picture/sleep2.png +0 -0
  27. package/picture/tui1.png +0 -0
  28. package/picture/tui2.png +0 -0
  29. package/picture/type1.png +0 -0
  30. package/picture/type2.png +0 -0
  31. package/scripts/bench-render.mjs +69 -0
  32. package/scripts/demo-working.mjs +130 -0
  33. package/scripts/demo.mjs +284 -0
  34. package/scripts/install-profile.mjs +174 -0
  35. package/scripts/mock-provider.mjs +211 -0
  36. package/src/cli/cellsize.js +120 -0
  37. package/src/cli/format.js +73 -0
  38. package/src/cli/highlight.js +932 -0
  39. package/src/cli/i18n.js +457 -0
  40. package/src/cli/main.js +630 -0
  41. package/src/cli/markdown.js +753 -0
  42. package/src/cli/renderer.js +1044 -0
  43. package/src/cli/screen.js +270 -0
  44. package/src/cli/theme.js +221 -0
  45. package/src/cli/view-state.js +396 -0
  46. package/src/cli/views.js +406 -0
  47. package/src/cli/width.js +337 -0
  48. package/src/cli/working/art.js +413 -0
  49. package/src/cli/working/main.js +569 -0
  50. package/src/cli/working/packing.js +159 -0
  51. package/src/cli/working/picker.js +75 -0
  52. package/src/cli/working/props.js +385 -0
  53. package/src/cli/working/scene.js +837 -0
  54. package/src/cli/working/sky.js +641 -0
  55. package/src/cli/working/sound.js +400 -0
  56. package/src/cli/working/state.js +528 -0
@@ -0,0 +1,413 @@
1
+ /**
2
+ * The orca, sampled from the reference artwork.
3
+ *
4
+ * Two things about the source art matter here:
5
+ *
6
+ * 1. It is a **sleeping** whale — the grey marks above it are three `Z`
7
+ * glyphs, not a water spout. They are *excluded* when sampling, so the
8
+ * sprite is the creature alone on a transparent background. The scene draws
9
+ * its own `z`s, and only while the orca is actually asleep.
10
+ * 2. Its native pixel grid is 64x40 (the artwork's blocks are 5 source pixels
11
+ * across). Sampling at that size keeps every row of the original; sampling
12
+ * smaller silently drops detail, and sampling larger only duplicates rows.
13
+ *
14
+ * Classes: `.` transparent, `b` body blue, `o` outline navy, `w` belly white.
15
+ *
16
+ * @module dsh-live-working/art
17
+ */
18
+
19
+ /** Sprite width in pixels (and cells: a cell is one pixel wide). */
20
+ export const ORCA_W = 64
21
+
22
+ /** Sprite height in pixels. A terminal cell holds two of these rows. */
23
+ /** The body's height. This is what the layout positions; the canvas is taller. */
24
+ export const ORCA_H = 40
25
+
26
+ /**
27
+ * The canvas the sprite is drawn on.
28
+ *
29
+ * Taller than the body on purpose. The flukes reach the last row of the body,
30
+ * so a tail with nowhere to go had no room to droop at all — every attempt to
31
+ * lay it down either ate pixels or left the shape ragged. The spare rows below
32
+ * are empty, invisible, and exactly what the tail needs to sink into.
33
+ */
34
+ export const ORCA_SPARE = 16
35
+ export const SPRITE_H = ORCA_H + ORCA_SPARE
36
+
37
+ /** The orca, exactly as sampled from the reference. */
38
+ const ORCA = [
39
+ '..........................................oo....................',
40
+ '..........................................oo....................',
41
+ '........................................oobboo..............oo..',
42
+ '........................................oobboo..............oo..',
43
+ '........................................oobbbboo..........oobboo',
44
+ '........................................oobbbboo..........oobboo',
45
+ '........................................oobbbbbboo....oooobbbboo',
46
+ '........................................oobbbbbboooooooooobbbboo',
47
+ '........................................oobbbbbbbboooobbbbbbbboo',
48
+ '........oooooooooooooooooo..............oobbbbbbbboooobbbbbbbboo',
49
+ '........oooooooooooooooooo................oobbbbbbbbbbbbbbbboo..',
50
+ '......oobbbbbbbbbbbbbbbbbboooo............oooobbbbbbbbbbbbbboo..',
51
+ '......oobbbbbbbbbbbbbbbbbboooo..............oobbbbbbbbbbbbbboo..',
52
+ '....oobbbbbbbbbbbbbbbbbbbbbbbboooo..........oobbbbbbbbbboooooo..',
53
+ '....oobbbbbbbbbbbbbbbbbbbbbbbboooo..........oobbbbbbbbbboooo....',
54
+ '..oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbboooo........oobbbbbboo........',
55
+ '..oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbboooo........oobbbbbboo........',
56
+ 'oooobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo....oobbbbbbbboo........',
57
+ 'oooobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo....oobbbbbbbboo........',
58
+ 'oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboooobbbbbbbbbboo........',
59
+ 'oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboooobbbbbbbbbboo........',
60
+ 'oobbbbbboobbbbbbbbbbbboobbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo........',
61
+ 'oobbbbbboobbbbbbbbbbbboobbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo........',
62
+ 'oobbbbbboobbbbbbbbbbbboobbbbbbbbbbbbbbbbbbbbbbbbbbbboo..........',
63
+ 'oobbbbbboobbbbbbbbbbbboobbbbbbbbbbbbbbbbbbbbbbbbbbbboo..........',
64
+ 'oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo..........',
65
+ 'oobbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbboo..........',
66
+ 'oobbbbbbbbwwwwwwwwwwwwwwbbbbbbbbbbbbbbbboobbbbbbbboo............',
67
+ 'oobbbbbbbbwwwwwwwwwwwwwwbbbbbbbbbbbbbbbboobbbbbbbboo............',
68
+ 'oooobbwwwwwwwwwwwwwwwwwwwwwwwwbbbbbbbbbbbboobbbbbboo............',
69
+ 'oooowwwwwwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbboooobboooo............',
70
+ '..oowwwwwwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbboooobboo..............',
71
+ '..oooowwwwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbbbboooooo..............',
72
+ '....oowwwwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbbbboooo................',
73
+ '......oooowwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbbbboooo..............',
74
+ '......oooowwwwwwwwwwwwwwwwwwwwwwwwoobbbbbbbbbboooo..............',
75
+ '..........oowwwwwwwwwwwwwwwwwwwwwwoooobbbbbbbbbbbboo............',
76
+ '..........oowwwwwwwwwwwwwwwwwwwwwwoooobbbbbbbbbbbboo............',
77
+ '............oooooooooooooooooooooo....oooooooooooooo............',
78
+ '............oooooooooooooooooooooo....oooooooooooooo............'
79
+ ]
80
+
81
+ /** A fresh, mutable copy of the orca at rest. */
82
+ export function orcaGrid() {
83
+ const body = ORCA.map((row) => row.split(''))
84
+ // Empty rows below, for the tail to droop into. Nothing else draws here.
85
+ for (let index = 0; index < ORCA_SPARE; index += 1) {
86
+ body.push(new Array(ORCA_W).fill('.'))
87
+ }
88
+ return body
89
+ }
90
+
91
+ /**
92
+ * The eyes, found in the sampled grid as two dark marks inside the head.
93
+ * Each is 2x4 pixels, which is enough to open, squint and close.
94
+ */
95
+ export const EYES = [
96
+ { x: 8, y: 21, width: 2, height: 4 },
97
+ { x: 22, y: 21, width: 2, height: 4 }
98
+ ]
99
+
100
+ /**
101
+ * Set both eyes.
102
+ *
103
+ * @param {string[][]} grid
104
+ * @param {'open' | 'squint' | 'closed' | 'happy'} mode
105
+ */
106
+ export function setEyes(grid, mode) {
107
+ for (const eye of EYES) {
108
+ const rows = eye.height
109
+ // Restore the body first so every mode is independent of the last.
110
+ for (let dy = 0; dy < rows; dy += 1) {
111
+ for (let dx = 0; dx < eye.width; dx += 1) grid[eye.y + dy][eye.x + dx] = 'b'
112
+ }
113
+ if (mode === 'open') {
114
+ for (let dy = 0; dy < rows; dy += 1) {
115
+ for (let dx = 0; dx < eye.width; dx += 1) grid[eye.y + dy][eye.x + dx] = 'o'
116
+ }
117
+ } else if (mode === 'squint') {
118
+ // Only the bottom pixel row survives: a contented narrowing.
119
+ for (let dx = 0; dx < eye.width; dx += 1) grid[eye.y + rows - 1][eye.x + dx] = 'o'
120
+ } else if (mode === 'closed') {
121
+ // One horizontal line, one pixel wider than the eye at each end, which
122
+ // reads as a shut eye rather than a dash.
123
+ for (let dx = -1; dx <= eye.width; dx += 1) grid[eye.y + rows - 1][eye.x + dx] = 'o'
124
+ } else if (mode === 'happy') {
125
+ // An upward arc.
126
+ grid[eye.y + rows - 2][eye.x] = 'o'
127
+ grid[eye.y + rows - 2][eye.x + eye.width - 1] = 'o'
128
+ for (let dx = 0; dx < eye.width; dx += 1) grid[eye.y + rows - 1][eye.x + dx] = 'o'
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Scale a pixel grid by a whole number, for a terminal with room to spare.
135
+ *
136
+ * Pixel art scales by whole numbers or not at all: a fractional scale produces
137
+ * uneven blocks that look like a rendering fault.
138
+ *
139
+ * @param {string[][]} grid
140
+ * @param {number} factor
141
+ * @returns {string[][]}
142
+ */
143
+ /**
144
+ * Where the tail begins.
145
+ *
146
+ * Not one column: the flukes' left edge wanders. The upper lobe starts at
147
+ * column 41, the lower one at 42, and only the middle rows — where the tail
148
+ * joins the back — begin at 45. Taking the whole tail to be everything right of
149
+ * a single column left the strip at 41..44 behind, which the eye reads as part
150
+ * of the tail sitting still while the rest of it moves.
151
+ *
152
+ * The middle rows are the exception for a reason: at rows 18..27 the body's own
153
+ * back reaches column 41, so including those columns there would drag the orca's
154
+ * back down with the tail.
155
+ */
156
+ /**
157
+ * The tail is **drawn**, one pose per state, not transformed.
158
+ *
159
+ * Six attempts at applying a transform to the single tail in the source art all
160
+ * traded one artefact for another: a shear frayed the edge, a fold tore the
161
+ * silhouette, a slide put the tail under the table, and every version left
162
+ * either a spike, a hole showing the background through, or both. The source
163
+ * art has one tail — an upright V — and no room anywhere to move it into. So
164
+ * there are two poses now, and the renderer picks one.
165
+ */
166
+ export const TAIL_X = 41
167
+
168
+ /** The leftmost tail column at a given row, in the original art. */
169
+ const TAIL_EDGES = (() => {
170
+ // Rows 0..18 have a clear gap between the body and the flukes, so their
171
+ // leading edge can simply be read: it is the first filled column from 40
172
+ // rightwards. Later rows merge with the body's own back and there is nothing
173
+ // to read, so they take a nominal edge.
174
+ const edges = []
175
+ for (let y = 0; y < ORCA.length; y += 1) {
176
+ if (y >= 19) {
177
+ edges.push(y < 28 ? 45 : 42)
178
+ continue
179
+ }
180
+ let left = -1
181
+ for (let x = 40; x < ORCA_W; x += 1) {
182
+ if (ORCA[y][x] !== '.') {
183
+ left = x
184
+ break
185
+ }
186
+ }
187
+ edges.push(left === -1 ? 40 : left)
188
+ }
189
+ return edges
190
+ })()
191
+
192
+ /**
193
+ * The leftmost column of the tail on a row.
194
+ *
195
+ * Not one column. The flukes' leading edge steps between 40 and 46 down the
196
+ * sprite, and taking it for a single value leaves a strip of tail behind when
197
+ * the tail is lifted out — a lone vertical line beside the orca that looks
198
+ * exactly like a transform artefact and is nothing of the sort.
199
+ */
200
+ export function tailLeftFor(y) {
201
+ return TAIL_EDGES[y] ?? 40
202
+ }
203
+
204
+ /**
205
+ * The tail as spans of `[row, firstColumn, lastColumn]`.
206
+ *
207
+ * `up` is lifted straight out of the source art, so the awake pose is unchanged.
208
+ * `sleep` is drawn: the tail curls round to the front, over the near side of
209
+ * the body, and tucks back in. Drawn art cannot lose a pixel or open a gap,
210
+ * which is the whole reason for doing it this way.
211
+ */
212
+ const TAIL_SPANS = {
213
+ up: null, // extracted from the art at load time
214
+ // The tail comes off the body's rear and sweeps down and forward along the
215
+ // desk, curling up at the front. The right end of every row reaches the rear,
216
+ // which is what joins it to the orca; drawing only the curl at the front left
217
+ // an oval floating on the belly with the body's back bare.
218
+ sleep: [
219
+ [30, 27, 45],
220
+ [31, 24, 45],
221
+ [32, 21, 44],
222
+ [33, 19, 44],
223
+ [34, 17, 43],
224
+ [35, 15, 43],
225
+ [36, 16, 42],
226
+ [37, 19, 41],
227
+ [38, 23, 40]
228
+ ]
229
+ }
230
+
231
+ /**
232
+ * How far back the body reaches, row by row, once the tail is out of the way.
233
+ *
234
+ * The source art's rear is a straight wall — the tail covered it, so it never
235
+ * showed. With the tail curled round to the front it is the last thing you see,
236
+ * and a flat vertical edge reads as a body that has been sliced off. These are
237
+ * the columns that round it.
238
+ */
239
+ export /** The column the rear's fill is normalised from. */
240
+ const INNER = 34
241
+
242
+ export const SLEEP_REAR = new Map([
243
+ [18, 40],
244
+ [19, 41],
245
+ [20, 42],
246
+ [21, 43],
247
+ [22, 43],
248
+ [23, 44],
249
+ [24, 44],
250
+ [25, 45],
251
+ [26, 45],
252
+ [27, 45],
253
+ [28, 45],
254
+ [29, 45],
255
+ [30, 45],
256
+ [31, 45],
257
+ [32, 44],
258
+ [33, 44],
259
+ [34, 43],
260
+ [35, 43],
261
+ [36, 42],
262
+ [37, 41],
263
+ [38, 40],
264
+ [39, 38]
265
+ ])
266
+
267
+ /** The awake tail, exactly as the source art has it. */
268
+ function liftedTail() {
269
+ const cells = []
270
+ for (let y = 0; y < ORCA.length; y += 1) {
271
+ for (let x = tailLeftFor(y); x < ORCA_W; x += 1) {
272
+ if (ORCA[y][x] !== '.') cells.push([x, y, ORCA[y][x]])
273
+ }
274
+ }
275
+ return cells
276
+ }
277
+
278
+ /** The sleeping tail: the drawn spans, with its edge generated. */
279
+ function sleepingTail() {
280
+ const cells = []
281
+ const spans = TAIL_SPANS.sleep
282
+ const filled = new Set()
283
+ for (const [y, from, to] of spans) {
284
+ for (let x = from; x <= to; x += 1) filled.add(`${x},${y}`)
285
+ }
286
+ /**
287
+ * A neighbour the tail would show an edge against.
288
+ *
289
+ * Unlike the flat pose this one lies *on* the body, so it keeps its outline
290
+ * everywhere: without an edge it would be blue on blue and simply not visible.
291
+ * The edge is what makes it read as a coil rather than a lump.
292
+ */
293
+ const exposed = (x, y) => !filled.has(`${x},${y}`)
294
+
295
+ for (const [y, from, to] of spans) {
296
+ for (let x = from; x <= to; x += 1) {
297
+ const edge =
298
+ exposed(x + 1, y) || exposed(x - 1, y) || exposed(x, y + 1) || exposed(x, y - 1)
299
+ cells.push([x, y, edge ? 'o' : 'b'])
300
+ }
301
+ }
302
+ return cells
303
+ }
304
+
305
+ const TAIL_CACHE = new Map()
306
+
307
+ /**
308
+ * The tail's pixels for a pose, filled and outlined.
309
+ *
310
+ * The outline is generated rather than drawn: a cell with an empty neighbour
311
+ * becomes the dark edge, which keeps the drawn pose's edge identical in
312
+ * treatment to the rest of the sprite.
313
+ *
314
+ * @param {'up' | 'sleep'} pose
315
+ * @returns {Map<string, string>} "x,y" -> class
316
+ */
317
+ export function tailPixels(pose) {
318
+ const key = pose === 'sleep' ? 'sleep' : 'up'
319
+ const hit = TAIL_CACHE.get(key)
320
+ if (hit !== undefined) return hit
321
+
322
+ const out = new Map()
323
+ for (const [x, y, cls] of key === 'sleep' ? sleepingTail() : liftedTail()) {
324
+ out.set(`${x},${y}`, cls)
325
+ }
326
+ TAIL_CACHE.set(key, out)
327
+ return out
328
+ }
329
+
330
+ /**
331
+ * The orca, with one of its tails.
332
+ *
333
+ * @param {{ pose?: 'up' | 'sleep', sway?: number }} [options] sway shifts the
334
+ * whole tail rigidly, which cannot open a gap because no column moves
335
+ * relative to another.
336
+ */
337
+ export function orcaWithTail(options = {}) {
338
+ const pose = options.pose === 'sleep' ? 'sleep' : 'up'
339
+ const sway = Number.isFinite(options.sway) ? Math.round(options.sway) : 0
340
+ const grid = ORCA.map((row) => row.split(''))
341
+ // Take the source art's own tail out; the pose supplies one instead.
342
+ for (let y = 0; y < grid.length; y += 1) {
343
+ for (let x = tailLeftFor(y); x < ORCA_W; x += 1) grid[y][x] = '.'
344
+ }
345
+ // With the tail gone, round the body's rear rather than leaving the wall the
346
+ // tail used to hide.
347
+ if (pose === 'sleep') {
348
+ for (const [y, rear] of SLEEP_REAR) {
349
+ const row = grid[y]
350
+ // Fill out to the curve, then cut everything past it. Filling matters:
351
+ // the source art's rear does not reach the curve on every row, and
352
+ // clearing alone left the edge stepping in and out with stray fill
353
+ // pixels caught between two runs of outline.
354
+ let last = -1
355
+ for (let x = 0; x < ORCA_W; x += 1) if (row[x] !== '.') last = x
356
+ for (let x = last + 1; x <= rear; x += 1) row[x] = 'b'
357
+ for (let x = rear + 1; x < ORCA_W; x += 1) row[x] = '.'
358
+ // One clean edge. The source art's rear carries stray outline pixels
359
+ // inside the silhouette, and once the curve is filled they read as a
360
+ // nick in the edge rather than as shading.
361
+ // Any outline still inside the filled band becomes fill, so each row has
362
+ // exactly one edge at the curve.
363
+ for (let x = INNER; x < rear; x += 1) if (row[x] === 'o') row[x] = 'b'
364
+ row[rear] = 'o'
365
+ }
366
+ }
367
+
368
+ // The spare rows go on first: a swayed tail needs somewhere to land, and
369
+ // appending them afterwards silently clipped whatever moved past the body.
370
+ for (let index = 0; index < ORCA_SPARE; index += 1) grid.push(new Array(ORCA_W).fill('.'))
371
+ // Where the tail meets the body. A rigid shift would pull the base away from
372
+ // the body and show the background through the seam, so the columns near the
373
+ // junction are drawn unshifted as well: the tail pivots instead of sliding.
374
+ const ANCHOR = 50
375
+ for (const [key, cls] of tailPixels(pose)) {
376
+ const [x, y] = key.split(',').map(Number)
377
+ if (sway !== 0 && x < ANCHOR) {
378
+ grid[y][x] = cls
379
+ continue
380
+ }
381
+ const target = y + sway
382
+ if (target < 0 || target >= grid.length) continue
383
+ grid[target][x] = cls
384
+ }
385
+ return grid
386
+ }
387
+
388
+ /** How far the tail swings on a frame, in pixels. */
389
+ export function tailSwayFor(kind, frame) {
390
+ if (kind === 'calling' || kind === 'ringing') return 0
391
+ const amp = kind === 'waiting' || kind === 'reading' ? 2.5 : 2
392
+ return Math.round(Math.sin(frame / 7) * amp)
393
+ }
394
+
395
+ /** How far along the settle to the sleeping pose is, 0 to 1. */
396
+ export function tailDroopFor(kind, sleepFor = 0) {
397
+ if (kind !== 'sleep') return 0
398
+ const settle = Math.max(0, Math.min(1, sleepFor / 16))
399
+ return settle * settle * (3 - 2 * settle)
400
+ }
401
+
402
+
403
+ export function scaleGrid(grid, factor) {
404
+ const n = Math.max(1, Math.trunc(factor))
405
+ if (n === 1) return grid.map((row) => row.slice())
406
+ const out = []
407
+ for (const row of grid) {
408
+ const grown = []
409
+ for (const cell of row) for (let index = 0; index < n; index += 1) grown.push(cell)
410
+ for (let index = 0; index < n; index += 1) out.push(grown.slice())
411
+ }
412
+ return out
413
+ }