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.
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/README.zh.md +790 -0
- package/assets/rain.ogg +0 -0
- package/bin/dsh-glyph-probe.js +51 -0
- package/bin/dsh-live-trace.js +29 -0
- package/bin/dsh-live-working.js +14 -0
- package/cordis.patch.yml +31 -0
- package/icon.svg +12 -0
- package/index.js +328 -0
- package/lib/client.js +178 -0
- package/lib/instance.js +68 -0
- package/lib/normalize.js +850 -0
- package/lib/paths.js +66 -0
- package/lib/protocol.js +115 -0
- package/lib/registry.js +232 -0
- package/lib/tools.js +257 -0
- package/lib/tracker.js +648 -0
- package/lib/transport.js +231 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -0
- package/picture/call1.png +0 -0
- package/picture/call2.png +0 -0
- package/picture/sleep1.png +0 -0
- package/picture/sleep2.png +0 -0
- package/picture/tui1.png +0 -0
- package/picture/tui2.png +0 -0
- package/picture/type1.png +0 -0
- package/picture/type2.png +0 -0
- package/scripts/bench-render.mjs +69 -0
- package/scripts/demo-working.mjs +130 -0
- package/scripts/demo.mjs +284 -0
- package/scripts/install-profile.mjs +174 -0
- package/scripts/mock-provider.mjs +211 -0
- package/src/cli/cellsize.js +120 -0
- package/src/cli/format.js +73 -0
- package/src/cli/highlight.js +932 -0
- package/src/cli/i18n.js +457 -0
- package/src/cli/main.js +630 -0
- package/src/cli/markdown.js +753 -0
- package/src/cli/renderer.js +1044 -0
- package/src/cli/screen.js +270 -0
- package/src/cli/theme.js +221 -0
- package/src/cli/view-state.js +396 -0
- package/src/cli/views.js +406 -0
- package/src/cli/width.js +337 -0
- package/src/cli/working/art.js +413 -0
- package/src/cli/working/main.js +569 -0
- package/src/cli/working/packing.js +159 -0
- package/src/cli/working/picker.js +75 -0
- package/src/cli/working/props.js +385 -0
- package/src/cli/working/scene.js +837 -0
- package/src/cli/working/sky.js +641 -0
- package/src/cli/working/sound.js +400 -0
- 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
|
+
}
|