bkper 5.2.0 → 5.3.1
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/lib/agent/extensions/bkper-ai-provider.d.ts.map +1 -1
- package/lib/agent/extensions/bkper-ai-provider.js +4 -6
- package/lib/agent/extensions/bkper-ai-provider.js.map +1 -1
- package/lib/agent/extensions/builtins.d.ts +2 -2
- package/lib/agent/extensions/builtins.d.ts.map +1 -1
- package/lib/agent/extensions/builtins.js.map +1 -1
- package/lib/agent/extensions/core-concepts-preload.d.ts +16 -4
- package/lib/agent/extensions/core-concepts-preload.d.ts.map +1 -1
- package/lib/agent/extensions/core-concepts-preload.js +78 -48
- package/lib/agent/extensions/core-concepts-preload.js.map +1 -1
- package/lib/agent/extensions/handoff-goal-editor.d.ts +1 -1
- package/lib/agent/extensions/handoff-goal-editor.d.ts.map +1 -1
- package/lib/agent/extensions/handoff-goal-editor.js +1 -1
- package/lib/agent/extensions/handoff-goal-editor.js.map +1 -1
- package/lib/agent/extensions/handoff.d.ts +1 -1
- package/lib/agent/extensions/handoff.d.ts.map +1 -1
- package/lib/agent/extensions/startup-logo.d.ts +4 -1
- package/lib/agent/extensions/startup-logo.d.ts.map +1 -1
- package/lib/agent/extensions/startup-logo.js +6 -1
- package/lib/agent/extensions/startup-logo.js.map +1 -1
- package/lib/agent/extensions/startup.d.ts +2 -2
- package/lib/agent/extensions/startup.d.ts.map +1 -1
- package/lib/agent/extensions/startup.js +21 -4
- package/lib/agent/extensions/startup.js.map +1 -1
- package/lib/agent/interactive/logo-3d.d.ts +125 -0
- package/lib/agent/interactive/logo-3d.d.ts.map +1 -0
- package/lib/agent/interactive/logo-3d.js +1187 -0
- package/lib/agent/interactive/logo-3d.js.map +1 -0
- package/lib/agent/interactive/prompt-history-search.js +1 -1
- package/lib/agent/interactive/prompt-history-search.js.map +1 -1
- package/lib/agent/interactive/prompt-history-shortcut.js +1 -1
- package/lib/agent/interactive/prompt-history-shortcut.js.map +1 -1
- package/lib/agent/pi-shared-modules.d.ts +21 -0
- package/lib/agent/pi-shared-modules.d.ts.map +1 -0
- package/lib/agent/pi-shared-modules.js +32 -0
- package/lib/agent/pi-shared-modules.js.map +1 -0
- package/lib/agent/startup-maintenance.d.ts +6 -1
- package/lib/agent/startup-maintenance.d.ts.map +1 -1
- package/lib/agent/startup-maintenance.js +9 -2
- package/lib/agent/startup-maintenance.js.map +1 -1
- package/lib/agent/system-prompt.d.ts.map +1 -1
- package/lib/agent/system-prompt.js +34 -12
- package/lib/agent/system-prompt.js.map +1 -1
- package/lib/upgrade/index.d.ts +2 -2
- package/lib/upgrade/index.d.ts.map +1 -1
- package/lib/upgrade/index.js +1 -1
- package/lib/upgrade/index.js.map +1 -1
- package/lib/upgrade/update-check.d.ts +8 -1
- package/lib/upgrade/update-check.d.ts.map +1 -1
- package/lib/upgrade/update-check.js +13 -6
- package/lib/upgrade/update-check.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { backgroundAnsi, colorToRgb, foregroundAnsi, getKeybindings, indexedColor, rgbColor, TuiAltScreen, visibleWidth, } from '../pi-shared-modules.js';
|
|
11
|
+
const DEPTH = 0.7;
|
|
12
|
+
const FLY_START = 0.1;
|
|
13
|
+
const FLY_DURATION = 1.3;
|
|
14
|
+
/** Braille dots per pixel when a model without an origin appears, growing from a speck at the center. */
|
|
15
|
+
const START_SCALE = 0.1;
|
|
16
|
+
function createModel(columns, rows, pixel, options) {
|
|
17
|
+
const blocks = [];
|
|
18
|
+
for (let row = 0; row < rows; row++) {
|
|
19
|
+
for (let column = 0; column < columns; column++) {
|
|
20
|
+
const color = pixel(column, row);
|
|
21
|
+
if (color)
|
|
22
|
+
blocks.push({ home: [column, row, 0], color });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
columns,
|
|
27
|
+
rows,
|
|
28
|
+
blocks,
|
|
29
|
+
cameraDistance: options.cameraDistance,
|
|
30
|
+
radius: Math.hypot(columns / 2, rows / 2, 1 + DEPTH / 2),
|
|
31
|
+
widthShare: options.widthShare,
|
|
32
|
+
puzzleMoves: random => options.puzzleMoves(random, blocks.length),
|
|
33
|
+
origin: options.origin,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The 3D model of a pixel logo that lifts off its half-block rendering at `origin`. */
|
|
37
|
+
export function createLogoModel(logo, origin) {
|
|
38
|
+
const columns = Math.max(...logo.pixels.map(line => line.length));
|
|
39
|
+
const rows = logo.pixels.length;
|
|
40
|
+
const colors = new Map(Object.entries(logo.colors).map(([pixel, color]) => [pixel, toRgb(color)]));
|
|
41
|
+
return createModel(columns, rows, (column, row) => { var _a, _b; return colors.get((_b = (_a = logo.pixels[row]) === null || _a === void 0 ? void 0 : _a[column]) !== null && _b !== void 0 ? _b : '.'); }, {
|
|
42
|
+
// Far enough that the perspective stays mild, as upstream does for its larger bitmaps.
|
|
43
|
+
cameraDistance: Math.max(10, Math.max(columns, rows) * 2.2),
|
|
44
|
+
widthShare: 0.4,
|
|
45
|
+
// A few percent of the blocks per step, so larger logos shuffle visibly but stay readable.
|
|
46
|
+
puzzleMoves: (random, blockCount) => Math.max(1, Math.round(blockCount * (0.03 + random * 0.04))),
|
|
47
|
+
origin,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// The sliding puzzle: after the model spun for a while, blocks slide into free neighboring cells, a few at a time.
|
|
51
|
+
// Each cycle shuffles, flies every block back home, and holds the model briefly.
|
|
52
|
+
const PUZZLE_START = FLY_START + FLY_DURATION + 3;
|
|
53
|
+
const PUZZLE_STEP = 0.3;
|
|
54
|
+
const PUZZLE_STEPS = 12;
|
|
55
|
+
const PUZZLE_RETURN = 1;
|
|
56
|
+
const PUZZLE_HOLD = 0.6;
|
|
57
|
+
const PUZZLE_CYCLE = PUZZLE_STEP * PUZZLE_STEPS + PUZZLE_RETURN + PUZZLE_HOLD;
|
|
58
|
+
const PUZZLE_MOVES = [
|
|
59
|
+
[1, 0, 0],
|
|
60
|
+
[-1, 0, 0],
|
|
61
|
+
[0, 1, 0],
|
|
62
|
+
[0, -1, 0],
|
|
63
|
+
[1, 0, 0],
|
|
64
|
+
[-1, 0, 0],
|
|
65
|
+
[0, 1, 0],
|
|
66
|
+
[0, -1, 0],
|
|
67
|
+
[0, 0, 1],
|
|
68
|
+
[0, 0, -1],
|
|
69
|
+
];
|
|
70
|
+
/** Block positions after each step of one shuffle cycle, starting at home. Deterministic per cycle. */
|
|
71
|
+
function shuffleSteps(model, cycle) {
|
|
72
|
+
const { columns, rows } = model;
|
|
73
|
+
let positions = model.blocks.map(block => block.home);
|
|
74
|
+
const steps = [positions];
|
|
75
|
+
const lastMoved = new Set();
|
|
76
|
+
let random = cycle * 7919 + 1;
|
|
77
|
+
const next = () => hash(random++);
|
|
78
|
+
const key = ([x, y, z]) => ((z + 1) * rows + y) * columns + x;
|
|
79
|
+
for (let step = 0; step < PUZZLE_STEPS; step++) {
|
|
80
|
+
const occupied = new Set(positions.map(key));
|
|
81
|
+
const nextPositions = [...positions];
|
|
82
|
+
const moved = new Set();
|
|
83
|
+
const moveCount = model.puzzleMoves(next());
|
|
84
|
+
for (let move = 0; move < moveCount; move++) {
|
|
85
|
+
const candidates = [];
|
|
86
|
+
for (let block = 0; block < positions.length; block++) {
|
|
87
|
+
if (moved.has(block))
|
|
88
|
+
continue;
|
|
89
|
+
const [x, y, z] = positions[block];
|
|
90
|
+
for (const [dx, dy, dz] of PUZZLE_MOVES) {
|
|
91
|
+
const target = [x + dx, y + dy, z + dz];
|
|
92
|
+
if (target[0] < 0 || target[0] >= columns || target[1] < 0 || target[1] >= rows)
|
|
93
|
+
continue;
|
|
94
|
+
if (target[2] < -1 || target[2] > 1 || occupied.has(key(target)))
|
|
95
|
+
continue;
|
|
96
|
+
candidates.push({ block, target });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Prefer blocks that did not just move, so the puzzle does not look like one block jittering.
|
|
100
|
+
const fresh = candidates.filter(candidate => !lastMoved.has(candidate.block));
|
|
101
|
+
const pool = fresh.length > 0 ? fresh : candidates;
|
|
102
|
+
const choice = pool[Math.floor(next() * pool.length)];
|
|
103
|
+
if (!choice)
|
|
104
|
+
break;
|
|
105
|
+
occupied.add(key(choice.target));
|
|
106
|
+
nextPositions[choice.block] = choice.target;
|
|
107
|
+
moved.add(choice.block);
|
|
108
|
+
}
|
|
109
|
+
lastMoved.clear();
|
|
110
|
+
for (const block of moved)
|
|
111
|
+
lastMoved.add(block);
|
|
112
|
+
positions = nextPositions;
|
|
113
|
+
steps.push(positions);
|
|
114
|
+
}
|
|
115
|
+
return steps;
|
|
116
|
+
}
|
|
117
|
+
const FRAME_MS = 1000 / 30;
|
|
118
|
+
const DUST_DURATION = 0.35;
|
|
119
|
+
const WAVE_SPREAD = 0.55;
|
|
120
|
+
const WAVE_JITTER = 0.12;
|
|
121
|
+
const DISSOLVE_END = FLY_START + WAVE_SPREAD + WAVE_JITTER + DUST_DURATION;
|
|
122
|
+
const BACKGROUND_FADE = 0.5;
|
|
123
|
+
const EXIT_DURATION = 1.1;
|
|
124
|
+
// A faint starfield fades in behind the logo once the screen has dissolved.
|
|
125
|
+
const STARS_START = 2.5;
|
|
126
|
+
const STARS_FADE = 2;
|
|
127
|
+
const STAR_DENSITY = 0.018;
|
|
128
|
+
// Star tints: neutral, pale blue, pale gold, and pale rose, each mixed half with the terminal's foreground.
|
|
129
|
+
const STAR_TINTS = [
|
|
130
|
+
[255, 255, 255],
|
|
131
|
+
[170, 195, 255],
|
|
132
|
+
[255, 225, 170],
|
|
133
|
+
[255, 190, 205],
|
|
134
|
+
];
|
|
135
|
+
const LOGO_COLOR_STEP = 5;
|
|
136
|
+
// On light backgrounds most of each braille cell shows the bright background, which washes the logo out. There,
|
|
137
|
+
// the logo gets a halo: its colors and coverage blurred into the cell backgrounds, strongest under the logo and
|
|
138
|
+
// fading out over HALO_RADIUS_X columns and HALO_RADIUS_Y rows (cells are about twice as tall as wide).
|
|
139
|
+
const LIGHT_HALO_STRENGTH = 0.3;
|
|
140
|
+
const HALO_RADIUS_X = 6;
|
|
141
|
+
const HALO_RADIUS_Y = 3;
|
|
142
|
+
const HALO_LEVELS = 32;
|
|
143
|
+
const STAR_ALPHA_LEVELS = 8;
|
|
144
|
+
const STAR_FLICKER_LEVELS = 3;
|
|
145
|
+
const HINT_FADE = 0.5;
|
|
146
|
+
// Braille dot bits, indexed by row * 2 + column within the 2x4 cell.
|
|
147
|
+
const DOT_BITS = [0x01, 0x08, 0x02, 0x10, 0x04, 0x20, 0x40, 0x80];
|
|
148
|
+
const BRAILLE = Array.from({ length: 256 }, (_, bits) => String.fromCharCode(0x2800 + bits));
|
|
149
|
+
const LIGHT = normalize([-0.45, -0.6, 0.75]);
|
|
150
|
+
const HALF_VECTOR = normalize([LIGHT[0], LIGHT[1], LIGHT[2] + 1]);
|
|
151
|
+
function normalize(v) {
|
|
152
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
153
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
154
|
+
}
|
|
155
|
+
function clamp01(value) {
|
|
156
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
157
|
+
}
|
|
158
|
+
/** Quintic smoothstep. */
|
|
159
|
+
function smooth(value) {
|
|
160
|
+
const t = clamp01(value);
|
|
161
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
162
|
+
}
|
|
163
|
+
function easeOut(value) {
|
|
164
|
+
const t = 1 - clamp01(value);
|
|
165
|
+
return 1 - t * t * t;
|
|
166
|
+
}
|
|
167
|
+
function mix(a, b, amount) {
|
|
168
|
+
return [
|
|
169
|
+
a[0] + (b[0] - a[0]) * amount,
|
|
170
|
+
a[1] + (b[1] - a[1]) * amount,
|
|
171
|
+
a[2] + (b[2] - a[2]) * amount,
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
function hash(value) {
|
|
175
|
+
let x = Math.imul(value ^ 0x9e3779b9, 0x85ebca6b);
|
|
176
|
+
x ^= x >>> 13;
|
|
177
|
+
x = Math.imul(x, 0xc2b2ae35);
|
|
178
|
+
x ^= x >>> 16;
|
|
179
|
+
return (x >>> 0) / 0x100000000;
|
|
180
|
+
}
|
|
181
|
+
/** Whether a background is light, by relative luminance. */
|
|
182
|
+
function isLight([r, g, b]) {
|
|
183
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 128;
|
|
184
|
+
}
|
|
185
|
+
function toRgb(color) {
|
|
186
|
+
const { r, g, b } = colorToRgb(color);
|
|
187
|
+
return [r, g, b];
|
|
188
|
+
}
|
|
189
|
+
/** Braille dots a glyph turns into, a rough measure of how much ink it has. */
|
|
190
|
+
function glyphInk(text, seed) {
|
|
191
|
+
if (text.trim() === '')
|
|
192
|
+
return 0;
|
|
193
|
+
if (/^[.,:;'`\-_·]$/.test(text))
|
|
194
|
+
return 2;
|
|
195
|
+
if (/^[\u2500-\u257f]$/.test(text))
|
|
196
|
+
return 3;
|
|
197
|
+
return 4 + Math.floor(seed * 3);
|
|
198
|
+
}
|
|
199
|
+
const segmenter = new Intl.Segmenter();
|
|
200
|
+
/** Parse rendered lines into cells with resolved colors. Other escape sequences (OSC, APC, cursor) are skipped. */
|
|
201
|
+
function parseScreen(lines, width, foreground, background) {
|
|
202
|
+
const palette = (index) => toRgb(indexedColor(index));
|
|
203
|
+
return lines.map(line => {
|
|
204
|
+
const cells = [];
|
|
205
|
+
let fg;
|
|
206
|
+
let bg;
|
|
207
|
+
let dim = false;
|
|
208
|
+
let inverse = false;
|
|
209
|
+
const applySgr = (params) => {
|
|
210
|
+
var _a, _b;
|
|
211
|
+
const codes = params === '' ? ['0'] : params.split(';');
|
|
212
|
+
for (let i = 0; i < codes.length; i++) {
|
|
213
|
+
const parts = codes[i].split(':').map(part => Number.parseInt(part, 10));
|
|
214
|
+
const code = (_a = parts[0]) !== null && _a !== void 0 ? _a : 0;
|
|
215
|
+
if (code === 38 || code === 48) {
|
|
216
|
+
// Extended color, either as `38;5;n` / `38;2;r;g;b` or colon sub-parameters.
|
|
217
|
+
let args;
|
|
218
|
+
if (parts.length > 1) {
|
|
219
|
+
args = parts.slice(1).filter(part => !Number.isNaN(part));
|
|
220
|
+
// `38:2::r:g:b` has an empty color space id, dropped above.
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const kind = Number.parseInt((_b = codes[i + 1]) !== null && _b !== void 0 ? _b : '', 10);
|
|
224
|
+
const count = kind === 5 ? 2 : kind === 2 ? 4 : 1;
|
|
225
|
+
args = codes
|
|
226
|
+
.slice(i + 1, i + 1 + count)
|
|
227
|
+
.map(part => Number.parseInt(part, 10));
|
|
228
|
+
i += count;
|
|
229
|
+
}
|
|
230
|
+
let color;
|
|
231
|
+
if (args[0] === 5 && args[1] !== undefined)
|
|
232
|
+
color = palette(args[1]);
|
|
233
|
+
else if (args[0] === 2 && args.length >= 4)
|
|
234
|
+
color = [args[1], args[2], args[3]];
|
|
235
|
+
if (color) {
|
|
236
|
+
if (code === 38)
|
|
237
|
+
fg = color;
|
|
238
|
+
else
|
|
239
|
+
bg = color;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (code === 0) {
|
|
243
|
+
fg = undefined;
|
|
244
|
+
bg = undefined;
|
|
245
|
+
dim = false;
|
|
246
|
+
inverse = false;
|
|
247
|
+
}
|
|
248
|
+
else if (code === 2)
|
|
249
|
+
dim = true;
|
|
250
|
+
else if (code === 22)
|
|
251
|
+
dim = false;
|
|
252
|
+
else if (code === 7)
|
|
253
|
+
inverse = true;
|
|
254
|
+
else if (code === 27)
|
|
255
|
+
inverse = false;
|
|
256
|
+
else if (code >= 30 && code <= 37)
|
|
257
|
+
fg = palette(code - 30);
|
|
258
|
+
else if (code >= 90 && code <= 97)
|
|
259
|
+
fg = palette(code - 90 + 8);
|
|
260
|
+
else if (code === 39)
|
|
261
|
+
fg = undefined;
|
|
262
|
+
else if (code >= 40 && code <= 47)
|
|
263
|
+
bg = palette(code - 40);
|
|
264
|
+
else if (code >= 100 && code <= 107)
|
|
265
|
+
bg = palette(code - 100 + 8);
|
|
266
|
+
else if (code === 49)
|
|
267
|
+
bg = undefined;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const pushText = (text) => {
|
|
271
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
272
|
+
const glyphWidth = visibleWidth(segment);
|
|
273
|
+
if (glyphWidth === 0 || cells.length + glyphWidth > width)
|
|
274
|
+
continue;
|
|
275
|
+
let cellFg = fg !== null && fg !== void 0 ? fg : foreground;
|
|
276
|
+
let cellBg = bg;
|
|
277
|
+
if (inverse) {
|
|
278
|
+
cellFg = bg !== null && bg !== void 0 ? bg : background;
|
|
279
|
+
cellBg = fg !== null && fg !== void 0 ? fg : foreground;
|
|
280
|
+
}
|
|
281
|
+
if (dim)
|
|
282
|
+
cellFg = mix(cellFg, cellBg !== null && cellBg !== void 0 ? cellBg : background, 0.4);
|
|
283
|
+
const cell = {
|
|
284
|
+
text: segment,
|
|
285
|
+
width: glyphWidth,
|
|
286
|
+
fg: cellFg,
|
|
287
|
+
bg: cellBg,
|
|
288
|
+
delay: 0,
|
|
289
|
+
ink: 0,
|
|
290
|
+
seed: 0,
|
|
291
|
+
};
|
|
292
|
+
cells.push(cell);
|
|
293
|
+
if (glyphWidth === 2)
|
|
294
|
+
cells.push(Object.assign(Object.assign({}, cell), { text: '', width: 0 }));
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
let i = 0;
|
|
298
|
+
while (i < line.length) {
|
|
299
|
+
const sequenceStart = line.indexOf('\x1b', i);
|
|
300
|
+
if (sequenceStart === -1) {
|
|
301
|
+
pushText(line.slice(i));
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
if (sequenceStart > i)
|
|
305
|
+
pushText(line.slice(i, sequenceStart));
|
|
306
|
+
const kind = line[sequenceStart + 1];
|
|
307
|
+
if (kind === '[') {
|
|
308
|
+
let end = sequenceStart + 2;
|
|
309
|
+
while (end < line.length &&
|
|
310
|
+
(line.charCodeAt(end) < 0x40 || line.charCodeAt(end) > 0x7e))
|
|
311
|
+
end++;
|
|
312
|
+
if (line[end] === 'm')
|
|
313
|
+
applySgr(line.slice(sequenceStart + 2, end));
|
|
314
|
+
i = end + 1;
|
|
315
|
+
}
|
|
316
|
+
else if (kind === ']' || kind === '_' || kind === 'P' || kind === '^') {
|
|
317
|
+
// String sequences end at BEL or ST (ESC \).
|
|
318
|
+
const bell = line.indexOf('\x07', sequenceStart + 2);
|
|
319
|
+
const st = line.indexOf('\x1b\\', sequenceStart + 2);
|
|
320
|
+
if (bell === -1 && st === -1)
|
|
321
|
+
break;
|
|
322
|
+
i = bell !== -1 && (st === -1 || bell < st) ? bell + 1 : st + 2;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
i = sequenceStart + 2;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return cells;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Row-major 3x3 rotation matrix for yaw (y), then pitch (x), then roll (z). */
|
|
332
|
+
function rotation(yaw, pitch, roll) {
|
|
333
|
+
const [sy, cy, sx, cx, sz, cz] = [
|
|
334
|
+
Math.sin(yaw),
|
|
335
|
+
Math.cos(yaw),
|
|
336
|
+
Math.sin(pitch),
|
|
337
|
+
Math.cos(pitch),
|
|
338
|
+
Math.sin(roll),
|
|
339
|
+
Math.cos(roll),
|
|
340
|
+
];
|
|
341
|
+
// Rz * Rx * Ry
|
|
342
|
+
const ry = [cy, 0, sy, 0, 1, 0, -sy, 0, cy];
|
|
343
|
+
const rx = [1, 0, 0, 0, cx, -sx, 0, sx, cx];
|
|
344
|
+
const rz = [cz, -sz, 0, sz, cz, 0, 0, 0, 1];
|
|
345
|
+
return multiply(rz, multiply(rx, ry));
|
|
346
|
+
}
|
|
347
|
+
function multiply(a, b) {
|
|
348
|
+
const result = new Array(9);
|
|
349
|
+
for (let row = 0; row < 3; row++) {
|
|
350
|
+
for (let column = 0; column < 3; column++) {
|
|
351
|
+
result[row * 3 + column] =
|
|
352
|
+
a[row * 3] * b[column] +
|
|
353
|
+
a[row * 3 + 1] * b[3 + column] +
|
|
354
|
+
a[row * 3 + 2] * b[6 + column];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
// The spin is locked to the puzzle: one turn per cycle, facing the camera in the middle of each hold.
|
|
360
|
+
const SPIN_RAMP = 1.4;
|
|
361
|
+
const SPIN_SPEED = (Math.PI * 2) / PUZZLE_CYCLE;
|
|
362
|
+
// How much the spin slows down while facing the camera (and speeds up while facing away), from 0 to 1.
|
|
363
|
+
const SPIN_LINGER = 0.6;
|
|
364
|
+
const FIRST_FRONT_VIEW = PUZZLE_START + PUZZLE_STEP * PUZZLE_STEPS + PUZZLE_RETURN + PUZZLE_HOLD / 2;
|
|
365
|
+
/** Uniform spin that accelerates from rest over SPIN_RAMP, then turns at SPIN_SPEED. */
|
|
366
|
+
function baseSpinPhase(time) {
|
|
367
|
+
const elapsed = time - FLY_START;
|
|
368
|
+
if (elapsed <= 0)
|
|
369
|
+
return 0;
|
|
370
|
+
const u = elapsed / SPIN_RAMP;
|
|
371
|
+
// Integral of the quintic smoothstep ramp.
|
|
372
|
+
if (u < 1)
|
|
373
|
+
return SPIN_SPEED * SPIN_RAMP * (Math.pow(u, 6) - 3 * Math.pow(u, 5) + 2.5 * Math.pow(u, 4));
|
|
374
|
+
return SPIN_SPEED * (SPIN_RAMP * 0.5 + elapsed - SPIN_RAMP);
|
|
375
|
+
}
|
|
376
|
+
// Extra rotation added during the flight, so the phase is a whole number of turns at every assembled front view.
|
|
377
|
+
const SPIN_ALIGNMENT = Math.PI * 2 - (baseSpinPhase(FIRST_FRONT_VIEW) % (Math.PI * 2));
|
|
378
|
+
/** Spin phase: whole turns exactly when the reassembled logo faces the camera. */
|
|
379
|
+
function spinPhase(time) {
|
|
380
|
+
return baseSpinPhase(time) + SPIN_ALIGNMENT * smooth((time - FLY_START) / FLY_DURATION);
|
|
381
|
+
}
|
|
382
|
+
/** Yaw of the logo. It matches the phase at whole turns but lingers there, so the logo reads from the front. */
|
|
383
|
+
function spinAngle(time) {
|
|
384
|
+
const phase = spinPhase(time);
|
|
385
|
+
return phase - SPIN_LINGER * Math.sin(phase);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Renders the model's blocks into braille cells. Only faces that point at the camera and are not covered by a
|
|
389
|
+
* touching block are drawn. Each face is rasterized over its projected bounds by intersecting each dot's ray
|
|
390
|
+
* with the face's plane, with a depth buffer resolving overlaps. Buffers are reused between frames.
|
|
391
|
+
*/
|
|
392
|
+
class BlockRaster {
|
|
393
|
+
constructor(cameraDistance) {
|
|
394
|
+
/** Braille dot bits per cell. */
|
|
395
|
+
this.bits = new Uint8Array(0);
|
|
396
|
+
/** Lit dots per cell. */
|
|
397
|
+
this.counts = new Uint8Array(0);
|
|
398
|
+
/** Summed RGB of the lit dots per cell. */
|
|
399
|
+
this.rgb = new Float32Array(0);
|
|
400
|
+
/** Halo tint strength per cell, from 0 to 1. Only set when a halo was requested. */
|
|
401
|
+
this.haloAmount = new Float32Array(0);
|
|
402
|
+
/** Halo color per cell. */
|
|
403
|
+
this.haloRgb = new Float32Array(0);
|
|
404
|
+
this.width = 0;
|
|
405
|
+
this.height = 0;
|
|
406
|
+
/** Ray parameter of the nearest hit per dot; smaller is nearer. */
|
|
407
|
+
this.depth = new Float32Array(0);
|
|
408
|
+
/** Face index + 1 of the nearest hit per dot, 0 for none. */
|
|
409
|
+
this.faceIds = new Uint16Array(0);
|
|
410
|
+
this.cameraDistance = cameraDistance;
|
|
411
|
+
}
|
|
412
|
+
render(width, height, pose, boxes, background, haloStrength) {
|
|
413
|
+
const dotWidth = width * 2;
|
|
414
|
+
const dotHeight = height * 4;
|
|
415
|
+
if (width !== this.width || height !== this.height) {
|
|
416
|
+
this.width = width;
|
|
417
|
+
this.height = height;
|
|
418
|
+
this.bits = new Uint8Array(width * height);
|
|
419
|
+
this.counts = new Uint8Array(width * height);
|
|
420
|
+
this.rgb = new Float32Array(width * height * 3);
|
|
421
|
+
this.haloAmount = new Float32Array(width * height);
|
|
422
|
+
this.haloRgb = new Float32Array(width * height * 3);
|
|
423
|
+
this.haloDirty = undefined;
|
|
424
|
+
this.depth = new Float32Array(dotWidth * dotHeight).fill(Infinity);
|
|
425
|
+
this.faceIds = new Uint16Array(dotWidth * dotHeight);
|
|
426
|
+
this.dirty = undefined;
|
|
427
|
+
}
|
|
428
|
+
else if (this.dirty) {
|
|
429
|
+
const { minX, minY, maxX, maxY } = this.dirty;
|
|
430
|
+
for (let row = minY; row <= maxY; row++) {
|
|
431
|
+
this.bits.fill(0, row * width + minX, row * width + maxX + 1);
|
|
432
|
+
this.counts.fill(0, row * width + minX, row * width + maxX + 1);
|
|
433
|
+
this.rgb.fill(0, (row * width + minX) * 3, (row * width + maxX + 1) * 3);
|
|
434
|
+
}
|
|
435
|
+
this.dirty = undefined;
|
|
436
|
+
}
|
|
437
|
+
if (this.haloDirty) {
|
|
438
|
+
const { minX, minY, maxX, maxY } = this.haloDirty;
|
|
439
|
+
for (let row = minY; row <= maxY; row++) {
|
|
440
|
+
this.haloAmount.fill(0, row * width + minX, row * width + maxX + 1);
|
|
441
|
+
this.haloRgb.fill(0, (row * width + minX) * 3, (row * width + maxX + 1) * 3);
|
|
442
|
+
}
|
|
443
|
+
this.haloDirty = undefined;
|
|
444
|
+
}
|
|
445
|
+
const m = rotation(pose.yaw, pose.pitch, pose.roll);
|
|
446
|
+
const { centerX, centerY, scale } = pose;
|
|
447
|
+
const cameraDistance = this.cameraDistance;
|
|
448
|
+
// The camera sits at (0, 0, cameraDistance) in camera space; object space is the transpose rotation.
|
|
449
|
+
const origin = [m[6] * cameraDistance, m[7] * cameraDistance, m[8] * cameraDistance];
|
|
450
|
+
const light = isLight(background);
|
|
451
|
+
const faces = this.visibleFaces(m, origin, pose, boxes, dotWidth, dotHeight, light);
|
|
452
|
+
if (faces.length === 0)
|
|
453
|
+
return;
|
|
454
|
+
let minX = dotWidth;
|
|
455
|
+
let minY = dotHeight;
|
|
456
|
+
let maxX = -1;
|
|
457
|
+
let maxY = -1;
|
|
458
|
+
for (const face of faces) {
|
|
459
|
+
minX = Math.min(minX, face.minX);
|
|
460
|
+
minY = Math.min(minY, face.minY);
|
|
461
|
+
maxX = Math.max(maxX, face.maxX);
|
|
462
|
+
maxY = Math.max(maxY, face.maxY);
|
|
463
|
+
}
|
|
464
|
+
if (maxX < minX || maxY < minY)
|
|
465
|
+
return;
|
|
466
|
+
const depth = this.depth;
|
|
467
|
+
const faceIds = this.faceIds;
|
|
468
|
+
for (let faceIndex = 0; faceIndex < faces.length; faceIndex++) {
|
|
469
|
+
const face = faces[faceIndex];
|
|
470
|
+
const a = face.axis;
|
|
471
|
+
const u = (a + 1) % 3;
|
|
472
|
+
const v = (a + 2) % 3;
|
|
473
|
+
const originA = origin[a];
|
|
474
|
+
const originU = origin[u];
|
|
475
|
+
const originV = origin[v];
|
|
476
|
+
// The ray direction in object space is linear in the dot position, so it is stepped per dot.
|
|
477
|
+
const stepA = m[a] / scale;
|
|
478
|
+
const stepU = m[u] / scale;
|
|
479
|
+
const stepV = m[v] / scale;
|
|
480
|
+
const sx = (face.minX + 0.5 - centerX) / scale;
|
|
481
|
+
for (let dotY = face.minY; dotY <= face.maxY; dotY++) {
|
|
482
|
+
const sy = (dotY + 0.5 - centerY) / scale;
|
|
483
|
+
let directionA = m[a] * sx + m[3 + a] * sy - m[6 + a] * cameraDistance;
|
|
484
|
+
let directionU = m[u] * sx + m[3 + u] * sy - m[6 + u] * cameraDistance;
|
|
485
|
+
let directionV = m[v] * sx + m[3 + v] * sy - m[6 + v] * cameraDistance;
|
|
486
|
+
let index = dotY * dotWidth + face.minX;
|
|
487
|
+
for (let dotX = face.minX; dotX <= face.maxX; dotX++, index++) {
|
|
488
|
+
const t = (face.plane - originA) / directionA;
|
|
489
|
+
if (t > 0 && t < depth[index]) {
|
|
490
|
+
const hitU = originU + t * directionU;
|
|
491
|
+
const hitV = originV + t * directionV;
|
|
492
|
+
if (hitU >= face.uMin &&
|
|
493
|
+
hitU <= face.uMax &&
|
|
494
|
+
hitV >= face.vMin &&
|
|
495
|
+
hitV <= face.vMax) {
|
|
496
|
+
depth[index] = t;
|
|
497
|
+
faceIds[index] = faceIndex + 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
directionA += stepA;
|
|
501
|
+
directionU += stepU;
|
|
502
|
+
directionV += stepV;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Shade the hit dots, pack them into braille cells, and reset the dot buffers for the next frame.
|
|
507
|
+
const bits = this.bits;
|
|
508
|
+
const counts = this.counts;
|
|
509
|
+
const rgb = this.rgb;
|
|
510
|
+
let cellMinX = width;
|
|
511
|
+
let cellMinY = height;
|
|
512
|
+
let cellMaxX = -1;
|
|
513
|
+
let cellMaxY = -1;
|
|
514
|
+
for (let dotY = minY; dotY <= maxY; dotY++) {
|
|
515
|
+
let index = dotY * dotWidth + minX;
|
|
516
|
+
for (let dotX = minX; dotX <= maxX; dotX++, index++) {
|
|
517
|
+
const id = faceIds[index];
|
|
518
|
+
if (id === 0)
|
|
519
|
+
continue;
|
|
520
|
+
const face = faces[id - 1];
|
|
521
|
+
// Points farther from the camera fade slightly toward the background for depth.
|
|
522
|
+
const fog = clamp01(0.15 - cameraDistance * (1 - depth[index]) * 0.12) * (light ? 0.5 : 1);
|
|
523
|
+
faceIds[index] = 0;
|
|
524
|
+
depth[index] = Infinity;
|
|
525
|
+
const cellX = dotX >> 1;
|
|
526
|
+
const cellY = dotY >> 2;
|
|
527
|
+
const cell = cellY * width + cellX;
|
|
528
|
+
bits[cell] |= DOT_BITS[(dotY & 3) * 2 + (dotX & 1)];
|
|
529
|
+
counts[cell]++;
|
|
530
|
+
rgb[cell * 3] += face.red + (background[0] - face.red) * fog;
|
|
531
|
+
rgb[cell * 3 + 1] += face.green + (background[1] - face.green) * fog;
|
|
532
|
+
rgb[cell * 3 + 2] += face.blue + (background[2] - face.blue) * fog;
|
|
533
|
+
if (cellX < cellMinX)
|
|
534
|
+
cellMinX = cellX;
|
|
535
|
+
if (cellX > cellMaxX)
|
|
536
|
+
cellMaxX = cellX;
|
|
537
|
+
if (cellY < cellMinY)
|
|
538
|
+
cellMinY = cellY;
|
|
539
|
+
if (cellY > cellMaxY)
|
|
540
|
+
cellMaxY = cellY;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (cellMaxX >= 0)
|
|
544
|
+
this.dirty = { minX: cellMinX, minY: cellMinY, maxX: cellMaxX, maxY: cellMaxY };
|
|
545
|
+
if (haloStrength > 0)
|
|
546
|
+
this.renderHalo(haloStrength);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Blur the logo's coverage and color over neighboring cells with a separable tent filter, so the tint falls
|
|
550
|
+
* off smoothly past the logo's edges.
|
|
551
|
+
*/
|
|
552
|
+
renderHalo(strength) {
|
|
553
|
+
const cells = this.dirty;
|
|
554
|
+
if (!cells)
|
|
555
|
+
return;
|
|
556
|
+
const { width, height, counts, rgb } = this;
|
|
557
|
+
const minX = Math.max(0, cells.minX - HALO_RADIUS_X);
|
|
558
|
+
const maxX = Math.min(width - 1, cells.maxX + HALO_RADIUS_X);
|
|
559
|
+
const minY = Math.max(0, cells.minY - HALO_RADIUS_Y);
|
|
560
|
+
const maxY = Math.min(height - 1, cells.maxY + HALO_RADIUS_Y);
|
|
561
|
+
const regionWidth = maxX - minX + 1;
|
|
562
|
+
const tent = (radius) => {
|
|
563
|
+
const weights = Array.from({ length: radius * 2 + 1 }, (_, i) => radius + 1 - Math.abs(i - radius));
|
|
564
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
565
|
+
return weights.map(weight => weight / total);
|
|
566
|
+
};
|
|
567
|
+
const weightsX = tent(HALO_RADIUS_X);
|
|
568
|
+
const weightsY = tent(HALO_RADIUS_Y);
|
|
569
|
+
// Horizontal pass over the rows that contain the logo: coverage and premultiplied color per cell.
|
|
570
|
+
const rows = cells.maxY - cells.minY + 1;
|
|
571
|
+
const horizontal = new Float32Array(rows * regionWidth * 4);
|
|
572
|
+
for (let y = cells.minY; y <= cells.maxY; y++) {
|
|
573
|
+
for (let x = minX; x <= maxX; x++) {
|
|
574
|
+
let coverage = 0;
|
|
575
|
+
let red = 0;
|
|
576
|
+
let green = 0;
|
|
577
|
+
let blue = 0;
|
|
578
|
+
for (let dx = -HALO_RADIUS_X; dx <= HALO_RADIUS_X; dx++) {
|
|
579
|
+
const sourceX = x + dx;
|
|
580
|
+
if (sourceX < cells.minX || sourceX > cells.maxX)
|
|
581
|
+
continue;
|
|
582
|
+
const source = y * width + sourceX;
|
|
583
|
+
const count = counts[source];
|
|
584
|
+
if (count === 0)
|
|
585
|
+
continue;
|
|
586
|
+
const weight = weightsX[dx + HALO_RADIUS_X] / 8;
|
|
587
|
+
coverage += count * weight;
|
|
588
|
+
red += rgb[source * 3] * weight;
|
|
589
|
+
green += rgb[source * 3 + 1] * weight;
|
|
590
|
+
blue += rgb[source * 3 + 2] * weight;
|
|
591
|
+
}
|
|
592
|
+
const target = ((y - cells.minY) * regionWidth + (x - minX)) * 4;
|
|
593
|
+
horizontal[target] = coverage;
|
|
594
|
+
horizontal[target + 1] = red;
|
|
595
|
+
horizontal[target + 2] = green;
|
|
596
|
+
horizontal[target + 3] = blue;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Vertical pass into the halo buffers.
|
|
600
|
+
for (let y = minY; y <= maxY; y++) {
|
|
601
|
+
for (let x = minX; x <= maxX; x++) {
|
|
602
|
+
let coverage = 0;
|
|
603
|
+
let red = 0;
|
|
604
|
+
let green = 0;
|
|
605
|
+
let blue = 0;
|
|
606
|
+
for (let dy = -HALO_RADIUS_Y; dy <= HALO_RADIUS_Y; dy++) {
|
|
607
|
+
const sourceY = y + dy;
|
|
608
|
+
if (sourceY < cells.minY || sourceY > cells.maxY)
|
|
609
|
+
continue;
|
|
610
|
+
const weight = weightsY[dy + HALO_RADIUS_Y];
|
|
611
|
+
const source = ((sourceY - cells.minY) * regionWidth + (x - minX)) * 4;
|
|
612
|
+
coverage += horizontal[source] * weight;
|
|
613
|
+
red += horizontal[source + 1] * weight;
|
|
614
|
+
green += horizontal[source + 2] * weight;
|
|
615
|
+
blue += horizontal[source + 3] * weight;
|
|
616
|
+
}
|
|
617
|
+
const target = y * width + x;
|
|
618
|
+
// Cells under the logo keep at least their own coverage, so faces stay solidly tinted.
|
|
619
|
+
const count = counts[target];
|
|
620
|
+
if (count > 0 && count / 8 > coverage) {
|
|
621
|
+
coverage = count / 8;
|
|
622
|
+
red = (rgb[target * 3] / count) * coverage;
|
|
623
|
+
green = (rgb[target * 3 + 1] / count) * coverage;
|
|
624
|
+
blue = (rgb[target * 3 + 2] / count) * coverage;
|
|
625
|
+
}
|
|
626
|
+
if (coverage <= 0)
|
|
627
|
+
continue;
|
|
628
|
+
// Quantized so neighboring cells usually share a background and its escape sequence.
|
|
629
|
+
const amount = Math.round(strength * Math.pow(Math.min(1, coverage), 0.7) * HALO_LEVELS) / HALO_LEVELS;
|
|
630
|
+
if (amount <= 0)
|
|
631
|
+
continue;
|
|
632
|
+
this.haloAmount[target] = amount;
|
|
633
|
+
this.haloRgb[target * 3] =
|
|
634
|
+
Math.round(red / coverage / LOGO_COLOR_STEP) * LOGO_COLOR_STEP;
|
|
635
|
+
this.haloRgb[target * 3 + 1] =
|
|
636
|
+
Math.round(green / coverage / LOGO_COLOR_STEP) * LOGO_COLOR_STEP;
|
|
637
|
+
this.haloRgb[target * 3 + 2] =
|
|
638
|
+
Math.round(blue / coverage / LOGO_COLOR_STEP) * LOGO_COLOR_STEP;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
this.haloDirty = { minX, minY, maxX, maxY };
|
|
642
|
+
}
|
|
643
|
+
visibleFaces(m, origin, pose, boxes, dotWidth, dotHeight, lightBackground) {
|
|
644
|
+
const faces = [];
|
|
645
|
+
const cameraDistance = this.cameraDistance;
|
|
646
|
+
for (const box of boxes) {
|
|
647
|
+
for (let a = 0; a < 3; a++) {
|
|
648
|
+
const u = (a + 1) % 3;
|
|
649
|
+
const v = (a + 2) % 3;
|
|
650
|
+
for (const side of [-1, 1]) {
|
|
651
|
+
const plane = side > 0 ? box.max[a] : box.min[a];
|
|
652
|
+
// Back faces point away from the camera.
|
|
653
|
+
if ((origin[a] - plane) * side <= 0)
|
|
654
|
+
continue;
|
|
655
|
+
// Faces pressed against a neighboring block are hidden. Positions are exact while blocks rest.
|
|
656
|
+
const covered = boxes.some(other => other !== box &&
|
|
657
|
+
(side > 0 ? other.min[a] : other.max[a]) === plane &&
|
|
658
|
+
other.min[u] <= box.min[u] &&
|
|
659
|
+
other.max[u] >= box.max[u] &&
|
|
660
|
+
other.min[v] <= box.min[v] &&
|
|
661
|
+
other.max[v] >= box.max[v]);
|
|
662
|
+
if (covered)
|
|
663
|
+
continue;
|
|
664
|
+
let minX = Infinity;
|
|
665
|
+
let minY = Infinity;
|
|
666
|
+
let maxX = -Infinity;
|
|
667
|
+
let maxY = -Infinity;
|
|
668
|
+
const corner = [0, 0, 0];
|
|
669
|
+
for (const cornerU of [box.min[u], box.max[u]]) {
|
|
670
|
+
for (const cornerV of [box.min[v], box.max[v]]) {
|
|
671
|
+
corner[a] = plane;
|
|
672
|
+
corner[u] = cornerU;
|
|
673
|
+
corner[v] = cornerV;
|
|
674
|
+
const x = m[0] * corner[0] + m[1] * corner[1] + m[2] * corner[2];
|
|
675
|
+
const y = m[3] * corner[0] + m[4] * corner[1] + m[5] * corner[2];
|
|
676
|
+
const z = m[6] * corner[0] + m[7] * corner[1] + m[8] * corner[2];
|
|
677
|
+
const perspective = (pose.scale * cameraDistance) / (cameraDistance - z);
|
|
678
|
+
const screenX = pose.centerX + x * perspective;
|
|
679
|
+
const screenY = pose.centerY + y * perspective;
|
|
680
|
+
minX = Math.min(minX, screenX);
|
|
681
|
+
minY = Math.min(minY, screenY);
|
|
682
|
+
maxX = Math.max(maxX, screenX);
|
|
683
|
+
maxY = Math.max(maxY, screenY);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const face = {
|
|
687
|
+
minX: Math.max(0, Math.floor(minX)),
|
|
688
|
+
minY: Math.max(0, Math.floor(minY)),
|
|
689
|
+
maxX: Math.min(dotWidth - 1, Math.ceil(maxX)),
|
|
690
|
+
maxY: Math.min(dotHeight - 1, Math.ceil(maxY)),
|
|
691
|
+
};
|
|
692
|
+
if (face.maxX < face.minX || face.maxY < face.minY)
|
|
693
|
+
continue;
|
|
694
|
+
// Faces are flat, so lighting is computed once per face. The normal in camera space is a
|
|
695
|
+
// column of the rotation matrix.
|
|
696
|
+
const nx = m[a] * side;
|
|
697
|
+
const ny = m[3 + a] * side;
|
|
698
|
+
const nz = m[6 + a] * side;
|
|
699
|
+
const diffuse = Math.max(0, nx * LIGHT[0] + ny * LIGHT[1] + nz * LIGHT[2]);
|
|
700
|
+
const rim = Math.max(0, nx * 0.8 - nz * 0.3);
|
|
701
|
+
// On light backgrounds, highlights toward white would vanish, so faces only get darker than
|
|
702
|
+
// the brand colors there.
|
|
703
|
+
const specular = lightBackground
|
|
704
|
+
? 0
|
|
705
|
+
: Math.pow(Math.max(0, nx * HALF_VECTOR[0] + ny * HALF_VECTOR[1] + nz * HALF_VECTOR[2]), 24) *
|
|
706
|
+
0.6 *
|
|
707
|
+
255;
|
|
708
|
+
const light = lightBackground
|
|
709
|
+
? Math.min(1, 0.55 + diffuse * 0.45 + rim * 0.1)
|
|
710
|
+
: 0.45 + diffuse * 0.78 + rim * 0.25;
|
|
711
|
+
faces.push(Object.assign(Object.assign({ axis: a, plane, uMin: box.min[u], uMax: box.max[u], vMin: box.min[v], vMax: box.max[v] }, face), { red: Math.min(255, box.color[0] * light + specular), green: Math.min(255, box.color[1] * light + specular), blue: Math.min(255, box.color[2] * light + specular) }));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return faces;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
let playing = false;
|
|
719
|
+
/**
|
|
720
|
+
* Plays the logo lifting off its half-block rendering at `origin`. Only fullscreen mode can show it, because it
|
|
721
|
+
* dissolves the rendered screen, which is captured right away. Returns false when it cannot play.
|
|
722
|
+
*/
|
|
723
|
+
export function playLogo3d(tui, theme, logo, origin) {
|
|
724
|
+
if (!(tui instanceof TuiAltScreen))
|
|
725
|
+
return false;
|
|
726
|
+
if (tui.hasOverlay())
|
|
727
|
+
return true;
|
|
728
|
+
const screen = tui.getScreenLines();
|
|
729
|
+
void showLogo3d(tui, theme, screen, createLogoModel(logo, origin)).catch(() => {
|
|
730
|
+
playing = false;
|
|
731
|
+
});
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Show the animation as a fullscreen overlay until it is dismissed. The overlay takes focus and mouse input and
|
|
736
|
+
* returns focus when hidden, so the rest of the UI keeps running underneath untouched.
|
|
737
|
+
*/
|
|
738
|
+
function showLogo3d(tui, theme, screen, model) {
|
|
739
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
740
|
+
if (playing)
|
|
741
|
+
return;
|
|
742
|
+
playing = true;
|
|
743
|
+
// Fading needs the terminal's actual default colors; the theme only knows its own.
|
|
744
|
+
const reported = yield tui.queryTerminalColors({ timeoutMs: 100 });
|
|
745
|
+
const dark = theme.appearance === 'dark';
|
|
746
|
+
const toRgbTuple = (rgb, fallback) => rgb ? [rgb.r, rgb.g, rgb.b] : fallback;
|
|
747
|
+
const background = toRgbTuple(reported.background, dark ? [0, 0, 0] : [255, 255, 255]);
|
|
748
|
+
const palette = {
|
|
749
|
+
foreground: toRgbTuple(reported.foreground, toRgb(theme.colors.text)),
|
|
750
|
+
background,
|
|
751
|
+
hintKey: toRgb(theme.colors.muted),
|
|
752
|
+
hintText: toRgb(theme.colors.dim),
|
|
753
|
+
colorMode: theme.getColorMode(),
|
|
754
|
+
};
|
|
755
|
+
if (tui.hasOverlay()) {
|
|
756
|
+
playing = false;
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const animation = new Logo3dAnimation(tui, screen, model, palette, () => {
|
|
760
|
+
playing = false;
|
|
761
|
+
overlay.hide();
|
|
762
|
+
});
|
|
763
|
+
const overlay = tui.showOverlay(animation, {
|
|
764
|
+
anchor: 'top-left',
|
|
765
|
+
width: '100%',
|
|
766
|
+
maxHeight: '100%',
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
export class Logo3dAnimation {
|
|
771
|
+
constructor(tui, screen, model, palette, onDone) {
|
|
772
|
+
var _a, _b;
|
|
773
|
+
this.startTime = performance.now();
|
|
774
|
+
this.lastRender = performance.now();
|
|
775
|
+
this.screenWidth = -1;
|
|
776
|
+
this.screenHeight = -1;
|
|
777
|
+
/** Star per cell index, or undefined. */
|
|
778
|
+
this.stars = [];
|
|
779
|
+
this.cells = [];
|
|
780
|
+
this.ansiCache = new Map();
|
|
781
|
+
this.tui = tui;
|
|
782
|
+
this.screen = screen;
|
|
783
|
+
this.model = model;
|
|
784
|
+
this.raster = new BlockRaster(model.cameraDistance);
|
|
785
|
+
this.foreground = palette.foreground;
|
|
786
|
+
this.background = palette.background;
|
|
787
|
+
this.palette = palette;
|
|
788
|
+
this.onDone = onDone;
|
|
789
|
+
this.timer = setInterval(() => {
|
|
790
|
+
// Also stop when no longer rendered, e.g. when pi hides all overlays on exit.
|
|
791
|
+
if ((this.exit && this.exitProgress() >= 1) ||
|
|
792
|
+
performance.now() - this.lastRender > 1000)
|
|
793
|
+
this.finish();
|
|
794
|
+
else
|
|
795
|
+
this.tui.requestRender();
|
|
796
|
+
}, FRAME_MS);
|
|
797
|
+
(_b = (_a = this.timer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
798
|
+
}
|
|
799
|
+
/** Play the exit animation. A second call skips it. */
|
|
800
|
+
close() {
|
|
801
|
+
if (this.exit) {
|
|
802
|
+
this.finish();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const time = this.elapsed();
|
|
806
|
+
const yaw = spinAngle(time);
|
|
807
|
+
const turn = Math.PI * 2;
|
|
808
|
+
this.exit = {
|
|
809
|
+
start: performance.now(),
|
|
810
|
+
time,
|
|
811
|
+
yaw,
|
|
812
|
+
targetYaw: Math.ceil(yaw / turn) * turn,
|
|
813
|
+
offsets: this.blockOffsets(time),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
handleInput(data) {
|
|
817
|
+
const keybindings = getKeybindings();
|
|
818
|
+
if (keybindings.matches(data, 'tui.select.cancel') ||
|
|
819
|
+
keybindings.matches(data, 'app.clear'))
|
|
820
|
+
this.close();
|
|
821
|
+
}
|
|
822
|
+
handleMouse(event) {
|
|
823
|
+
if (event.type === 'click')
|
|
824
|
+
this.close();
|
|
825
|
+
return { handled: true, render: false };
|
|
826
|
+
}
|
|
827
|
+
invalidate() {
|
|
828
|
+
this.screenWidth = -1;
|
|
829
|
+
this.screenHeight = -1;
|
|
830
|
+
this.ansiCache.clear();
|
|
831
|
+
}
|
|
832
|
+
render(width) {
|
|
833
|
+
var _a, _b, _c;
|
|
834
|
+
const height = Math.max(1, this.tui.terminal.rows);
|
|
835
|
+
const foreground = this.foreground;
|
|
836
|
+
const background = this.background;
|
|
837
|
+
this.lastRender = performance.now();
|
|
838
|
+
if (width !== this.screenWidth || height !== this.screenHeight) {
|
|
839
|
+
this.screenWidth = width;
|
|
840
|
+
this.screenHeight = height;
|
|
841
|
+
this.cells = this.prepareCells(width, foreground);
|
|
842
|
+
this.stars = this.prepareStars(width, height);
|
|
843
|
+
}
|
|
844
|
+
// Dissolve time runs forward on entry and backward on exit, so the screen reassembles in reverse order.
|
|
845
|
+
let dissolveTime;
|
|
846
|
+
let pose;
|
|
847
|
+
let hintAlpha;
|
|
848
|
+
let starAlpha;
|
|
849
|
+
let offsets;
|
|
850
|
+
if (this.exit) {
|
|
851
|
+
const progress = this.exitProgress();
|
|
852
|
+
const landing = clamp01(progress / 0.8);
|
|
853
|
+
const settle = 1 - smooth(landing);
|
|
854
|
+
dissolveTime = Math.min(this.exit.time, DISSOLVE_END) * (1 - progress);
|
|
855
|
+
pose = this.pose(width, height, this.flyProgress(this.exit.time) * settle, this.exit.time);
|
|
856
|
+
pose.yaw = this.exit.yaw + (this.exit.targetYaw - this.exit.yaw) * easeOut(landing);
|
|
857
|
+
pose.pitch *= settle;
|
|
858
|
+
pose.roll *= settle;
|
|
859
|
+
hintAlpha = this.hintAlpha(this.exit.time) * (1 - smooth(progress / 0.2));
|
|
860
|
+
starAlpha = this.starAlpha(this.exit.time) * (1 - smooth(progress / 0.3));
|
|
861
|
+
// Blocks are home well before the logo lands.
|
|
862
|
+
const gather = 1 - smooth(progress / 0.5);
|
|
863
|
+
offsets = this.exit.offsets.map(([x, y, z]) => [x * gather, y * gather, z * gather]);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
const time = this.elapsed();
|
|
867
|
+
dissolveTime = time;
|
|
868
|
+
pose = this.pose(width, height, this.flyProgress(time), time);
|
|
869
|
+
hintAlpha = this.hintAlpha(time);
|
|
870
|
+
starAlpha = this.starAlpha(time);
|
|
871
|
+
offsets = this.blockOffsets(time);
|
|
872
|
+
}
|
|
873
|
+
const { columns, rows } = this.model;
|
|
874
|
+
const boxes = this.model.blocks.map((block, index) => {
|
|
875
|
+
const [dx, dy, dz] = offsets[index];
|
|
876
|
+
const x = block.home[0] - columns / 2 + dx;
|
|
877
|
+
const y = block.home[1] - rows / 2 + dy;
|
|
878
|
+
return {
|
|
879
|
+
min: [x, y, dz - DEPTH / 2],
|
|
880
|
+
max: [x + 1, y + 1, dz + DEPTH / 2],
|
|
881
|
+
color: block.color,
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
const raster = this.raster;
|
|
885
|
+
raster.render(width, height, pose, boxes, background, isLight(background) ? LIGHT_HALO_STRENGTH : 0);
|
|
886
|
+
const halo = (index, base) => {
|
|
887
|
+
const amount = raster.haloAmount[index];
|
|
888
|
+
if (amount <= 0)
|
|
889
|
+
return base;
|
|
890
|
+
const color = raster.haloRgb;
|
|
891
|
+
return mix(base !== null && base !== void 0 ? base : background, [color[index * 3], color[index * 3 + 1], color[index * 3 + 2]], amount);
|
|
892
|
+
};
|
|
893
|
+
const backgroundFade = smooth(dissolveTime / BACKGROUND_FADE);
|
|
894
|
+
const textFade = smooth(dissolveTime / 0.6) * 0.3;
|
|
895
|
+
// Once the screen has fully dissolved, the text layer is empty and can be skipped.
|
|
896
|
+
const textActive = dissolveTime < DISSOLVE_END || backgroundFade < 1;
|
|
897
|
+
const hint = this.hint(width, height, hintAlpha, background);
|
|
898
|
+
const frame = Math.floor(dissolveTime * 14);
|
|
899
|
+
const now = this.elapsed();
|
|
900
|
+
// Star brightness is quantized, so a row only changes when one of its stars steps to another level
|
|
901
|
+
// instead of on every frame.
|
|
902
|
+
const starLevel = Math.round(starAlpha * STAR_ALPHA_LEVELS) / STAR_ALPHA_LEVELS;
|
|
903
|
+
const starfield = (index) => {
|
|
904
|
+
if (starLevel <= 0)
|
|
905
|
+
return undefined;
|
|
906
|
+
const star = this.stars[index];
|
|
907
|
+
if (!star)
|
|
908
|
+
return undefined;
|
|
909
|
+
const flicker = Math.round((0.5 + 0.5 * Math.sin(now * star.speed + star.phase)) * STAR_FLICKER_LEVELS) / STAR_FLICKER_LEVELS;
|
|
910
|
+
return {
|
|
911
|
+
text: star.glyph,
|
|
912
|
+
fg: mix(background, mix(foreground, star.tint, 0.5), star.strength * (0.7 + 0.3 * flicker) * starLevel),
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
const lines = [];
|
|
916
|
+
for (let row = 0; row < height; row++) {
|
|
917
|
+
const cells = (_a = this.cells[row]) !== null && _a !== void 0 ? _a : [];
|
|
918
|
+
let line = '';
|
|
919
|
+
let currentFg = -1;
|
|
920
|
+
let currentBg = -1;
|
|
921
|
+
const emit = (text, fg, bg) => {
|
|
922
|
+
const fgKey = fg ? this.pack(fg) : -1;
|
|
923
|
+
const bgKey = bg ? this.pack(bg) : -1;
|
|
924
|
+
if (fgKey !== currentFg && text !== ' ') {
|
|
925
|
+
line += fg ? this.ansi(fgKey, false) : '\x1b[39m';
|
|
926
|
+
currentFg = fgKey;
|
|
927
|
+
}
|
|
928
|
+
if (bgKey !== currentBg) {
|
|
929
|
+
line += bg ? this.ansi(bgKey, true) : '\x1b[49m';
|
|
930
|
+
currentBg = bgKey;
|
|
931
|
+
}
|
|
932
|
+
line += text;
|
|
933
|
+
};
|
|
934
|
+
for (let column = 0; column < width; column++) {
|
|
935
|
+
const index = row * width + column;
|
|
936
|
+
const cell = textActive ? cells[column] : undefined;
|
|
937
|
+
const cellBg = halo(index, (cell === null || cell === void 0 ? void 0 : cell.bg) && backgroundFade < 1
|
|
938
|
+
? mix(cell.bg, background, backgroundFade)
|
|
939
|
+
: undefined);
|
|
940
|
+
if (hint &&
|
|
941
|
+
row === hint.row &&
|
|
942
|
+
column >= hint.start &&
|
|
943
|
+
column < hint.start + hint.text.length) {
|
|
944
|
+
const offset = column - hint.start;
|
|
945
|
+
emit(hint.text[offset], offset < hint.keyLength ? hint.keyColor : hint.color, cellBg);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const dots = raster.bits[index];
|
|
949
|
+
if (dots) {
|
|
950
|
+
// Quantized so neighboring cells on one face usually share a color and its escape sequence.
|
|
951
|
+
const step = LOGO_COLOR_STEP * raster.counts[index];
|
|
952
|
+
const rgb = raster.rgb;
|
|
953
|
+
const color = [
|
|
954
|
+
Math.round(rgb[index * 3] / step) * LOGO_COLOR_STEP,
|
|
955
|
+
Math.round(rgb[index * 3 + 1] / step) * LOGO_COLOR_STEP,
|
|
956
|
+
Math.round(rgb[index * 3 + 2] / step) * LOGO_COLOR_STEP,
|
|
957
|
+
];
|
|
958
|
+
emit(BRAILLE[dots], color, cellBg);
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!cell) {
|
|
962
|
+
const star = starfield(index);
|
|
963
|
+
emit((_b = star === null || star === void 0 ? void 0 : star.text) !== null && _b !== void 0 ? _b : ' ', star === null || star === void 0 ? void 0 : star.fg, cellBg);
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const dust = (dissolveTime - cell.delay) / DUST_DURATION;
|
|
967
|
+
if (dust < 0 && cell.width === 2 && !raster.bits[index + 1]) {
|
|
968
|
+
emit(cell.text, mix(cell.fg, background, textFade), cellBg);
|
|
969
|
+
column++;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (dust < 0 && cell.width === 1) {
|
|
973
|
+
emit(cell.text, mix(cell.fg, background, textFade), cellBg);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const dotCount = dust < 0 ? cell.ink : Math.round(cell.ink * (1 - clamp01(dust)));
|
|
977
|
+
if (dotCount <= 0) {
|
|
978
|
+
const star = starfield(index);
|
|
979
|
+
emit((_c = star === null || star === void 0 ? void 0 : star.text) !== null && _c !== void 0 ? _c : ' ', star === null || star === void 0 ? void 0 : star.fg, cellBg);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
// Pick `dotCount` distinct dots; the choice changes a few times per second so the dust shimmers.
|
|
983
|
+
let bits = 0;
|
|
984
|
+
let placed = 0;
|
|
985
|
+
for (let attempt = 0; placed < dotCount && attempt < 32; attempt++) {
|
|
986
|
+
const bit = DOT_BITS[Math.floor(hash(cell.seed * 7919 + frame * 131 + attempt) * 8)];
|
|
987
|
+
if (bits & bit)
|
|
988
|
+
continue;
|
|
989
|
+
bits |= bit;
|
|
990
|
+
placed++;
|
|
991
|
+
}
|
|
992
|
+
const fade = textFade + (1 - textFade) * Math.pow(clamp01(dust), 0.8);
|
|
993
|
+
emit(BRAILLE[bits], mix(cell.fg, background, fade), cellBg);
|
|
994
|
+
}
|
|
995
|
+
lines.push(`${line}\x1b[0m`);
|
|
996
|
+
}
|
|
997
|
+
return lines;
|
|
998
|
+
}
|
|
999
|
+
elapsed() {
|
|
1000
|
+
return (performance.now() - this.startTime) / 1000;
|
|
1001
|
+
}
|
|
1002
|
+
exitProgress() {
|
|
1003
|
+
return this.exit
|
|
1004
|
+
? clamp01((performance.now() - this.exit.start) / 1000 / EXIT_DURATION)
|
|
1005
|
+
: 0;
|
|
1006
|
+
}
|
|
1007
|
+
finish() {
|
|
1008
|
+
if (!this.timer)
|
|
1009
|
+
return;
|
|
1010
|
+
clearInterval(this.timer);
|
|
1011
|
+
this.timer = undefined;
|
|
1012
|
+
this.onDone();
|
|
1013
|
+
}
|
|
1014
|
+
/** Each block's displacement from its place in the model at `time`, in grid units. */
|
|
1015
|
+
blockOffsets(time) {
|
|
1016
|
+
var _a;
|
|
1017
|
+
const blocks = this.model.blocks;
|
|
1018
|
+
const home = () => blocks.map(() => [0, 0, 0]);
|
|
1019
|
+
if (time < PUZZLE_START)
|
|
1020
|
+
return home();
|
|
1021
|
+
const cycle = Math.floor((time - PUZZLE_START) / PUZZLE_CYCLE);
|
|
1022
|
+
const local = time - PUZZLE_START - cycle * PUZZLE_CYCLE;
|
|
1023
|
+
if (((_a = this.shuffle) === null || _a === void 0 ? void 0 : _a.cycle) !== cycle)
|
|
1024
|
+
this.shuffle = { cycle, steps: shuffleSteps(this.model, cycle) };
|
|
1025
|
+
const steps = this.shuffle.steps;
|
|
1026
|
+
const offset = (position, index) => {
|
|
1027
|
+
const block = blocks[index].home;
|
|
1028
|
+
return [position[0] - block[0], position[1] - block[1], position[2] - block[2]];
|
|
1029
|
+
};
|
|
1030
|
+
const shuffleEnd = PUZZLE_STEP * PUZZLE_STEPS;
|
|
1031
|
+
if (local < shuffleEnd) {
|
|
1032
|
+
const step = Math.floor(local / PUZZLE_STEP);
|
|
1033
|
+
const progress = smooth((local - step * PUZZLE_STEP) / PUZZLE_STEP);
|
|
1034
|
+
return steps[step].map((from, index) => {
|
|
1035
|
+
const a = offset(from, index);
|
|
1036
|
+
const b = offset(steps[step + 1][index], index);
|
|
1037
|
+
return [
|
|
1038
|
+
a[0] + (b[0] - a[0]) * progress,
|
|
1039
|
+
a[1] + (b[1] - a[1]) * progress,
|
|
1040
|
+
a[2] + (b[2] - a[2]) * progress,
|
|
1041
|
+
];
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
if (local < shuffleEnd + PUZZLE_RETURN) {
|
|
1045
|
+
// All blocks fly home at once. Alternating arcs in depth keep them from passing through each other.
|
|
1046
|
+
const u = (local - shuffleEnd) / PUZZLE_RETURN;
|
|
1047
|
+
const remaining = 1 - smooth(u);
|
|
1048
|
+
const arc = Math.sin(Math.PI * clamp01(u)) * 0.8;
|
|
1049
|
+
return steps[PUZZLE_STEPS].map((position, index) => {
|
|
1050
|
+
const [x, y, z] = offset(position, index);
|
|
1051
|
+
return [
|
|
1052
|
+
x * remaining,
|
|
1053
|
+
y * remaining,
|
|
1054
|
+
z * remaining + (index % 2 === 0 ? arc : -arc),
|
|
1055
|
+
];
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return home();
|
|
1059
|
+
}
|
|
1060
|
+
flyProgress(time) {
|
|
1061
|
+
return smooth((time - FLY_START) / FLY_DURATION);
|
|
1062
|
+
}
|
|
1063
|
+
starAlpha(time) {
|
|
1064
|
+
return smooth((time - STARS_START) / STARS_FADE);
|
|
1065
|
+
}
|
|
1066
|
+
hintAlpha(time) {
|
|
1067
|
+
return smooth((time - FLY_START - FLY_DURATION) / HINT_FADE);
|
|
1068
|
+
}
|
|
1069
|
+
pose(width, height, progress, time) {
|
|
1070
|
+
const { columns, rows, cameraDistance, radius, widthShare, origin } = this.model;
|
|
1071
|
+
const reach = radius * (cameraDistance / (cameraDistance - radius));
|
|
1072
|
+
// Lifting off, the front face must cover exactly the half-block cells (2x2 dots per pixel) despite the
|
|
1073
|
+
// perspective.
|
|
1074
|
+
const startScale = origin
|
|
1075
|
+
? (2 * (cameraDistance - DEPTH / 2)) / cameraDistance
|
|
1076
|
+
: START_SCALE;
|
|
1077
|
+
const endScale = Math.max(startScale, Math.min(width * 2 * widthShare, (height * 4 - 8) * 0.48) / reach);
|
|
1078
|
+
const endX = width;
|
|
1079
|
+
const endY = height * 2 - 2;
|
|
1080
|
+
const startX = origin ? origin.column * 2 + columns : endX;
|
|
1081
|
+
const startY = origin ? origin.row * 4 + rows : endY;
|
|
1082
|
+
return {
|
|
1083
|
+
centerX: startX + (endX - startX) * progress,
|
|
1084
|
+
centerY: startY + (endY - startY) * progress,
|
|
1085
|
+
// Interpolate the zoom geometrically so it feels uniform.
|
|
1086
|
+
scale: startScale * Math.pow((endScale / startScale), progress),
|
|
1087
|
+
yaw: spinAngle(time),
|
|
1088
|
+
// Tilts are zero at whole turns, so the camera looks straight at the front of the reassembled logo.
|
|
1089
|
+
pitch: 0.3 * Math.sin(spinPhase(time)) * progress,
|
|
1090
|
+
roll: 0.06 * Math.sin(2 * spinPhase(time)) * progress,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
hint(width, height, alpha, background) {
|
|
1094
|
+
var _a;
|
|
1095
|
+
if (alpha <= 0)
|
|
1096
|
+
return undefined;
|
|
1097
|
+
const key = (_a = getKeybindings().getKeys('tui.select.cancel')[0]) !== null && _a !== void 0 ? _a : 'escape';
|
|
1098
|
+
const text = `${key} to return`;
|
|
1099
|
+
if (text.length > width)
|
|
1100
|
+
return undefined;
|
|
1101
|
+
return {
|
|
1102
|
+
row: height - 2,
|
|
1103
|
+
start: Math.floor((width - text.length) / 2),
|
|
1104
|
+
text,
|
|
1105
|
+
keyLength: key.length,
|
|
1106
|
+
keyColor: mix(background, this.palette.hintKey, alpha),
|
|
1107
|
+
color: mix(background, this.palette.hintText, alpha),
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
/** A sparse, deterministic starfield: one braille dot in about STAR_DENSITY of all cells. */
|
|
1111
|
+
prepareStars(width, height) {
|
|
1112
|
+
const stars = new Array(width * height);
|
|
1113
|
+
for (let index = 0; index < width * height; index++) {
|
|
1114
|
+
if (hash(index * 3 + 0x51ed) >= STAR_DENSITY)
|
|
1115
|
+
continue;
|
|
1116
|
+
const random = (salt) => hash(index * 7 + salt * 0x9e37);
|
|
1117
|
+
stars[index] = {
|
|
1118
|
+
glyph: BRAILLE[DOT_BITS[Math.floor(random(1) * 8)]],
|
|
1119
|
+
tint: STAR_TINTS[Math.floor(random(2) * STAR_TINTS.length)],
|
|
1120
|
+
strength: 0.1 + random(3) * 0.12,
|
|
1121
|
+
phase: random(4) * Math.PI * 2,
|
|
1122
|
+
speed: 0.4 + random(5) * 0.8,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
return stars;
|
|
1126
|
+
}
|
|
1127
|
+
prepareCells(width, foreground) {
|
|
1128
|
+
const cells = parseScreen(this.screen, width, foreground, this.background);
|
|
1129
|
+
const height = Math.max(1, this.tui.terminal.rows);
|
|
1130
|
+
const { columns, rows, origin } = this.model;
|
|
1131
|
+
// The dust spreads out from where the model starts.
|
|
1132
|
+
const centerX = origin ? origin.column + columns / 2 : width / 2;
|
|
1133
|
+
const centerY = origin ? origin.row + rows / 4 : height / 2;
|
|
1134
|
+
const farthest = Math.hypot(Math.max(centerX, width - centerX), Math.max(centerY, height - centerY) * 2);
|
|
1135
|
+
for (let row = 0; row < cells.length; row++) {
|
|
1136
|
+
const line = cells[row];
|
|
1137
|
+
for (let column = 0; column < line.length; column++) {
|
|
1138
|
+
const cell = line[column];
|
|
1139
|
+
// Wide graphemes share their first column's timing so both halves change together.
|
|
1140
|
+
const owner = cell.width === 0 ? line[column - 1] : cell;
|
|
1141
|
+
const seed = hash(row * 65537 + column);
|
|
1142
|
+
if (cell.width !== 0) {
|
|
1143
|
+
const distance = Math.hypot(column - centerX, (row - centerY) * 2) / farthest;
|
|
1144
|
+
cell.delay = FLY_START + distance * WAVE_SPREAD + seed * WAVE_JITTER;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
cell.delay = owner.delay;
|
|
1148
|
+
}
|
|
1149
|
+
cell.seed = row * 65537 + column;
|
|
1150
|
+
cell.ink = glyphInk(owner.text, seed);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// The 3D model replaces its half-block rendering on screen.
|
|
1154
|
+
if (origin) {
|
|
1155
|
+
for (let row = origin.row; row < origin.row + Math.ceil(rows / 2); row++) {
|
|
1156
|
+
const line = cells[row];
|
|
1157
|
+
if (!line)
|
|
1158
|
+
continue;
|
|
1159
|
+
for (let column = origin.column; column < origin.column + columns; column++) {
|
|
1160
|
+
const cell = line[column];
|
|
1161
|
+
if (cell) {
|
|
1162
|
+
cell.text = ' ';
|
|
1163
|
+
cell.width = 1;
|
|
1164
|
+
cell.ink = 0;
|
|
1165
|
+
cell.bg = undefined;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return cells;
|
|
1171
|
+
}
|
|
1172
|
+
pack(color) {
|
|
1173
|
+
return (Math.round(color[0]) << 16) | (Math.round(color[1]) << 8) | Math.round(color[2]);
|
|
1174
|
+
}
|
|
1175
|
+
ansi(key, isBackground) {
|
|
1176
|
+
const cacheKey = key * 2 + (isBackground ? 1 : 0);
|
|
1177
|
+
let value = this.ansiCache.get(cacheKey);
|
|
1178
|
+
if (value === undefined) {
|
|
1179
|
+
const color = rgbColor((key >> 16) & 255, (key >> 8) & 255, key & 255);
|
|
1180
|
+
const mode = this.palette.colorMode;
|
|
1181
|
+
value = isBackground ? backgroundAnsi(color, mode) : foregroundAnsi(color, mode);
|
|
1182
|
+
this.ansiCache.set(cacheKey, value);
|
|
1183
|
+
}
|
|
1184
|
+
return value;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
//# sourceMappingURL=logo-3d.js.map
|