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