tokenmaw 0.4.0 → 0.4.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/README.md +0 -1
- package/dist/cli.js +16 -0
- package/dist/runtime/agent-runtime.js +2 -43
- package/dist/runtime/agent-store.js +29 -0
- package/dist/ui/commands.js +0 -1
- package/dist/ui/fullscreen-tui.js +123 -38
- package/dist/ui/welcome.js +430 -19
- package/dist/update-check.js +332 -0
- package/package.json +8 -2
package/dist/ui/welcome.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** The big animated wordmark. Every theme plays its own one-shot opening
|
|
2
|
+
* act — under a second, each with a distinct mechanic — before the loop
|
|
3
|
+
* takes over: a ping-ponging shine for most themes, glyph rain for the
|
|
4
|
+
* phosphor one. All motion is a pure function of the frame, the mark is
|
|
5
|
+
* born from nothing (frame 0 paints nothing anywhere), each cell is
|
|
6
|
+
* monotonic — once painted, never erased — and the intro's final frame
|
|
7
|
+
* always equals the settled loop frame, so the handoff is invisible. */
|
|
2
8
|
import { activeTuiTheme } from './theme.js';
|
|
3
9
|
const GLYPH_LETTERS = [
|
|
4
10
|
['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
|
|
@@ -19,11 +25,16 @@ const LOGO_WIDTH = LOGO[0].length;
|
|
|
19
25
|
const BRAND = 'T O K E N M A W';
|
|
20
26
|
const TAGLINE = 'multi-agent coding runtime';
|
|
21
27
|
const RAIN_GLYPHS = '01ハヒクシアウトナニヌネモリ';
|
|
22
|
-
|
|
28
|
+
/** The whole opening act must finish inside a second: at 20 fps that is
|
|
29
|
+
* exactly 20 frames. The intro covers frames 0..19; the loop's own clock
|
|
30
|
+
* would render frame 20 with the shine band just off the left edge —
|
|
31
|
+
* exactly where frame 19 hands over — so the seam is invisible. */
|
|
32
|
+
export const INTRO_DURATION = 20;
|
|
33
|
+
export function hexToRgb(hex) {
|
|
23
34
|
const value = hex.replace('#', '');
|
|
24
35
|
return [parseInt(value.slice(0, 2), 16), parseInt(value.slice(2, 4), 16), parseInt(value.slice(4, 6), 16)];
|
|
25
36
|
}
|
|
26
|
-
function mix(from, to, t) {
|
|
37
|
+
export function mix(from, to, t) {
|
|
27
38
|
const a = hexToRgb(from);
|
|
28
39
|
const b = hexToRgb(to);
|
|
29
40
|
return '#' + a.map((channel, index) => Math.round(channel + (b[index] - channel) * t).toString(16).padStart(2, '0')).join('');
|
|
@@ -38,6 +49,387 @@ function flicker(frame, seed) {
|
|
|
38
49
|
x ^= x >>> 16;
|
|
39
50
|
return (x >>> 0) / 4294967296;
|
|
40
51
|
}
|
|
52
|
+
/** Stateless hash → [0,1), keyed by the seed alone. Chain seeds for variety. */
|
|
53
|
+
export function hash01(seed) {
|
|
54
|
+
let x = (Math.imul(seed + 1, 2654435761) ^ Math.imul(0x9e3779b9, 97531)) >>> 0;
|
|
55
|
+
x ^= x >>> 16;
|
|
56
|
+
x = Math.imul(x, 0x45d9f3b);
|
|
57
|
+
x ^= x >>> 16;
|
|
58
|
+
x = Math.imul(x, 0x45d9f3b);
|
|
59
|
+
x ^= x >>> 16;
|
|
60
|
+
return (x >>> 0) / 4294967296;
|
|
61
|
+
}
|
|
62
|
+
/** Width of the shine's gaussian band, in diagonal cells. */
|
|
63
|
+
export const SHINE_BAND_WIDTH = 18;
|
|
64
|
+
/** Peak glow the band paints over the settled gradient. */
|
|
65
|
+
export const SHINE_GLOW_BOOST = 0.7;
|
|
66
|
+
/**
|
|
67
|
+
* Center of the shine band along the diagonal axis for loop-frame u. The band
|
|
68
|
+
* ping-pongs with a swaying rate: it lingers at the ends, then dashes across.
|
|
69
|
+
* At u = 0 the band sits just off the left edge — the loop always re-enters
|
|
70
|
+
* from the left, and the default theme's brush intro is exactly this phase's
|
|
71
|
+
* first sweep, so the handoff is seamless.
|
|
72
|
+
*/
|
|
73
|
+
export function shineBandCenter(u, span) {
|
|
74
|
+
const theta = 0.04 * u + 3.5 * Math.sin(u / 170) - Math.PI / 2;
|
|
75
|
+
return span / 2 + Math.sin(theta) * (span / 2 + 6);
|
|
76
|
+
}
|
|
77
|
+
/** The loop's settled cell with the shine band centered at `bandCenter`. */
|
|
78
|
+
function cellAtBand(context, bandCenter) {
|
|
79
|
+
const dx = context.diagonal - bandCenter;
|
|
80
|
+
const glow = Math.exp(-(dx * dx) / SHINE_BAND_WIDTH);
|
|
81
|
+
const intensity = Math.min(0.95, glow * SHINE_GLOW_BOOST);
|
|
82
|
+
const color = intensity > 0.03 ? mix(context.base, context.headingStrong, intensity) : context.base;
|
|
83
|
+
return { glyph: context.glyph, color };
|
|
84
|
+
}
|
|
85
|
+
/** The loop state an intro must land on: the shine band where it sits at the
|
|
86
|
+
* moment the intro hands over. Every intro converges to this. */
|
|
87
|
+
function settledCell(context) {
|
|
88
|
+
return cellAtBand(context, shineBandCenter(context.duration, context.span));
|
|
89
|
+
}
|
|
90
|
+
/** True while the welcome intro is playing and cells should be handed to the
|
|
91
|
+
* intro renderer instead of the settled shine loop. */
|
|
92
|
+
export function introActive(frame) {
|
|
93
|
+
return frame < INTRO_DURATION;
|
|
94
|
+
}
|
|
95
|
+
/** Deterministic global birth order for a logo cell: 0..N-1 across the whole
|
|
96
|
+
* mark, from a fixed hash of the cell coordinates. Intros that key off
|
|
97
|
+
* "when this cell is due" add their own shaping on top of this. */
|
|
98
|
+
function birthOrder(context, seed) {
|
|
99
|
+
const cells = context.width * 6;
|
|
100
|
+
return Math.floor(hash01(Math.floor((context.rowIndex * context.width + context.column) * 7.13 + seed * 7919)) * cells);
|
|
101
|
+
}
|
|
102
|
+
/** Per-theme opening acts, keyed by theme name. Each is a pure function of
|
|
103
|
+
* the frame, paints nothing at frame 0, and converges on the settled cell by
|
|
104
|
+
* the handoff frame. */
|
|
105
|
+
const intros = {
|
|
106
|
+
// aurora — the shine band is the brush: a reveal front sweeps left to
|
|
107
|
+
// right across the mark, painting the settled palette in its wake with a
|
|
108
|
+
// faint trailing sheen at the edge. The front starts off the left edge so
|
|
109
|
+
// frame 0 is blank, and the colors are the settled ones, so the handoff
|
|
110
|
+
// is invisible.
|
|
111
|
+
aurora: {
|
|
112
|
+
cell(context) {
|
|
113
|
+
const front = (context.frame / (context.duration - 1)) * (context.span + 4) - 2;
|
|
114
|
+
if (context.diagonal > front)
|
|
115
|
+
return null; // ahead of the brush: still nothing
|
|
116
|
+
const dx = front - context.diagonal;
|
|
117
|
+
const sheen = Math.exp(-(dx * dx) / 6) * 0.45;
|
|
118
|
+
const settled = settledCell(context);
|
|
119
|
+
const color = sheen > 0.02 ? mix(settled.color, context.accent, sheen) : settled.color;
|
|
120
|
+
return { glyph: context.glyph, color };
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
// midnight — a narrow spotlight sweeps across a black stage; each cell the
|
|
124
|
+
// beam touches blazes white-hot at birth, then cools into the settled
|
|
125
|
+
// palette. Left to right, one pass.
|
|
126
|
+
midnight: {
|
|
127
|
+
cell(context) {
|
|
128
|
+
const front = context.progress * (context.span + 10) - 5;
|
|
129
|
+
if (context.diagonal > front)
|
|
130
|
+
return null;
|
|
131
|
+
const dx = context.diagonal - front;
|
|
132
|
+
const birth = Math.exp(-(dx * dx) / 3);
|
|
133
|
+
const settled = settledCell(context);
|
|
134
|
+
const color = birth > 0.02 ? mix(settled.color, '#ffffff', Math.min(1, birth * 1.4)) : settled.color;
|
|
135
|
+
return { glyph: context.glyph, color };
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
// nord — an aurora curtain: a cold green-white wave washes left to right
|
|
139
|
+
// through the mark, each row's edge slightly offset so the reveal ripples
|
|
140
|
+
// like light on ice.
|
|
141
|
+
nord: {
|
|
142
|
+
cell(context) {
|
|
143
|
+
const wave = context.progress * (context.span + 14) - 7;
|
|
144
|
+
const jitter = (context.rnd(1) - 0.5) * 5 + context.rowIndex * 2.2;
|
|
145
|
+
const edge = wave + jitter;
|
|
146
|
+
if (context.diagonal > edge)
|
|
147
|
+
return null;
|
|
148
|
+
const dx = context.diagonal - edge;
|
|
149
|
+
const glow = Math.exp(-(dx * dx) / 6);
|
|
150
|
+
const settled = settledCell(context);
|
|
151
|
+
const color = glow > 0.02 ? mix(settled.color, '#a8ffda', glow * 0.8) : settled.color;
|
|
152
|
+
return { glyph: context.glyph, color };
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
// dracula — an ink bleed: dark violet droplets surface at fixed points and
|
|
156
|
+
// spread outward as circles until the mark is flooded; each cell is born
|
|
157
|
+
// wet-glossy pink and dries into the settled gradient.
|
|
158
|
+
dracula: {
|
|
159
|
+
cell(context) {
|
|
160
|
+
const seeds = [3, 11, 23, 37, 53];
|
|
161
|
+
let born;
|
|
162
|
+
for (const seed of seeds) {
|
|
163
|
+
const cx = context.rnd(seed * 2) * context.span;
|
|
164
|
+
const cy = context.rnd(seed * 3) * 6;
|
|
165
|
+
const dist = Math.hypot(context.diagonal - cx, (context.rowIndex - cy) * 1.6);
|
|
166
|
+
const start = context.rnd(seed) * 0.3;
|
|
167
|
+
const reach = start + (dist / Math.max(1, context.span * 0.75)) * 0.6;
|
|
168
|
+
if (context.progress >= reach && (born === undefined || reach < born))
|
|
169
|
+
born = reach;
|
|
170
|
+
}
|
|
171
|
+
if (born === undefined)
|
|
172
|
+
return null;
|
|
173
|
+
const wet = Math.max(0, 1 - (context.progress - born) / 0.14);
|
|
174
|
+
const settled = settledCell(context);
|
|
175
|
+
const color = wet > 0 ? mix(settled.color, '#ff79c6', wet * 0.75) : settled.color;
|
|
176
|
+
return { glyph: context.glyph, color };
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
// dawn — a sunrise: cells wake from the bottom row upward, each igniting
|
|
180
|
+
// gold as the light line passes and cooling into the settled pastel.
|
|
181
|
+
dawn: {
|
|
182
|
+
cell(context) {
|
|
183
|
+
const due = 0.06 + ((5 - context.rowIndex) / 6) * 0.72 + (context.rnd(2) - 0.5) * 0.06;
|
|
184
|
+
if (context.progress < due)
|
|
185
|
+
return null;
|
|
186
|
+
const heat = Math.max(0, 1 - (context.progress - due) / 0.15);
|
|
187
|
+
const settled = settledCell(context);
|
|
188
|
+
const color = heat > 0.02 ? mix(settled.color, '#ffb347', heat * 0.7) : settled.color;
|
|
189
|
+
return { glyph: context.glyph, color };
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
// solarized — a darkroom print developing: cells surface as a faint cyan
|
|
193
|
+
// ghost that deepens into full contrast, center cells first like a
|
|
194
|
+
// vignette developing from the middle out.
|
|
195
|
+
solarized: {
|
|
196
|
+
cell(context) {
|
|
197
|
+
const vignette = 1 - Math.abs(context.diagonal - context.span / 2) / context.span;
|
|
198
|
+
const due = 0.1 + (1 - vignette) * 0.5;
|
|
199
|
+
if (context.progress < due)
|
|
200
|
+
return null;
|
|
201
|
+
const develop = Math.min(1, (context.progress - due) / 0.2);
|
|
202
|
+
const settled = settledCell(context);
|
|
203
|
+
return { glyph: context.glyph, color: mix(mix(context.accent, '#000000', 0.8), settled.color, develop) };
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
// rose-pine — petals fall: pale rose petals drift down through the mark's
|
|
207
|
+
// bounding box and stick where they land, filling the letterforms from
|
|
208
|
+
// random contact points until every cell has settled.
|
|
209
|
+
'rose-pine': {
|
|
210
|
+
cell(context) {
|
|
211
|
+
const seeds = [7, 19, 31, 43, 59, 71];
|
|
212
|
+
let landed;
|
|
213
|
+
for (const seed of seeds) {
|
|
214
|
+
const drift = context.rnd(seed) * context.span;
|
|
215
|
+
const delay = context.rnd(seed * 5) * 0.24;
|
|
216
|
+
const fall = delay + (1 - (context.rowIndex + (context.rnd(seed * 3) - 0.5) * 2) / 6) * 0.4;
|
|
217
|
+
if (context.progress >= fall && Math.abs(context.diagonal - drift) < 7 + context.progress * 10) {
|
|
218
|
+
if (landed === undefined || fall < landed)
|
|
219
|
+
landed = fall;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (landed === undefined)
|
|
223
|
+
return null;
|
|
224
|
+
const settle = Math.max(0, 1 - (context.progress - landed) / 0.12);
|
|
225
|
+
const settled = settledCell(context);
|
|
226
|
+
const color = settle > 0 ? mix(settled.color, '#eb6f92', settle * 0.65) : settled.color;
|
|
227
|
+
return { glyph: context.glyph, color };
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
// tokyo-night — neon ignition: the sign flickers on like a faulty neon
|
|
231
|
+
// tube — cells pop in a random order, sputtering briefly before holding
|
|
232
|
+
// steady.
|
|
233
|
+
'tokyo-night': {
|
|
234
|
+
cell(context) {
|
|
235
|
+
const due = 0.04 + (birthOrder(context, 13) / (context.width * 6)) * 0.62;
|
|
236
|
+
if (context.progress < due)
|
|
237
|
+
return null;
|
|
238
|
+
const fresh = context.progress - due;
|
|
239
|
+
if (fresh < 0.14) {
|
|
240
|
+
const sputter = context.rnd(Math.floor(fresh * 140) * 13 + context.rowIndex * 31 + context.column);
|
|
241
|
+
const settled = settledCell(context);
|
|
242
|
+
// A dying tube dims to near-black instead of vanishing: the cell
|
|
243
|
+
// stays painted, the neon still reads as sputtering.
|
|
244
|
+
if (sputter < 0.4)
|
|
245
|
+
return { glyph: context.glyph, color: mix(settled.color, '#16161e', 0.88) };
|
|
246
|
+
return { glyph: context.glyph, color: mix(settled.color, '#7dcfff', 0.5) };
|
|
247
|
+
}
|
|
248
|
+
return settledCell(context);
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
// catppuccin-mocha — a ripple: a soft ring expands from the center of the
|
|
252
|
+
// mark, painting cells as it passes and leaving a brief pastel afterglow.
|
|
253
|
+
'catppuccin-mocha': {
|
|
254
|
+
cell(context) {
|
|
255
|
+
const ring = context.progress * (context.span * 0.75 + 8);
|
|
256
|
+
const dist = Math.hypot(context.diagonal - context.span / 2, (context.rowIndex - 2.5) * 2.4);
|
|
257
|
+
if (dist > ring)
|
|
258
|
+
return null;
|
|
259
|
+
const afterglow = Math.max(0, 1 - (ring - dist) / 7);
|
|
260
|
+
const settled = settledCell(context);
|
|
261
|
+
const color = afterglow > 0.02 ? mix(settled.color, '#cba6f7', afterglow * 0.55) : settled.color;
|
|
262
|
+
return { glyph: context.glyph, color };
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
// catppuccin-latte — a coffee pour: the cup fills bottom-up like an
|
|
266
|
+
// espresso being pulled, the surface line wobbling as it rises; each
|
|
267
|
+
// dunked cell flashes with a steamed-milk sheen that fades back into
|
|
268
|
+
// the settled pastel as the surface moves on.
|
|
269
|
+
'catppuccin-latte': {
|
|
270
|
+
cell(context) {
|
|
271
|
+
const level = -0.4 + context.progress * 6 + Math.sin(context.progress * 12 + context.column / 6) * 0.2;
|
|
272
|
+
if (context.rowIndex + (context.rnd(4) - 0.5) * 0.5 > level)
|
|
273
|
+
return null;
|
|
274
|
+
const flash = Math.exp(-Math.max(0, level - context.rowIndex) / 1.5);
|
|
275
|
+
const settled = settledCell(context);
|
|
276
|
+
const color = flash > 0.02 ? mix(settled.color, context.accent, flash * 0.6) : settled.color;
|
|
277
|
+
return { glyph: context.glyph, color };
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
// gruvbox-dark — a CRT power-on: the mark snaps in as a horizontal line
|
|
281
|
+
// that stretches vertically from the center outward, phosphor-bright at
|
|
282
|
+
// the growing edges, then relaxes into the settled warm palette.
|
|
283
|
+
'gruvbox-dark': {
|
|
284
|
+
cell(context) {
|
|
285
|
+
const half = context.progress * 4.2;
|
|
286
|
+
if (Math.abs(context.rowIndex - 2.5) > half)
|
|
287
|
+
return null;
|
|
288
|
+
const edge = Math.abs(Math.abs(context.rowIndex - 2.5) - half);
|
|
289
|
+
const bloom = edge < 0.9 ? 1 - edge / 0.9 : 0;
|
|
290
|
+
const settled = settledCell(context);
|
|
291
|
+
const color = bloom > 0 ? mix(settled.color, '#fe8019', bloom * 0.8) : settled.color;
|
|
292
|
+
return { glyph: context.glyph, color };
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
// one-dark — a typewriter: the mark is typed cell by cell, left to right,
|
|
296
|
+
// top to bottom, each keystroke landing full-strength with no fade.
|
|
297
|
+
'one-dark': {
|
|
298
|
+
cell(context) {
|
|
299
|
+
const order = context.rowIndex * context.width + context.column;
|
|
300
|
+
const total = 6 * context.width;
|
|
301
|
+
const typed = Math.floor((context.progress / 0.94) * total);
|
|
302
|
+
if (order >= typed)
|
|
303
|
+
return null; // strictly after 0: nothing is typed at frame 0
|
|
304
|
+
const settled = settledCell(context);
|
|
305
|
+
const fresh = order === typed - 1 ? 1 : 0;
|
|
306
|
+
const color = fresh ? mix(settled.color, '#98c379', 0.45) : settled.color;
|
|
307
|
+
return { glyph: context.glyph, color };
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
// monokai — a pixel mosaic: cells pop in as dim colored blocks in random
|
|
311
|
+
// order, dwell a few ticks scintillating, then resolve one by one into
|
|
312
|
+
// the true glyph with a brief bright flash.
|
|
313
|
+
monokai: {
|
|
314
|
+
cell(context) {
|
|
315
|
+
const pop = 0.05 + (birthOrder(context, 9) / (context.width * 6)) * 0.55;
|
|
316
|
+
if (context.progress <= pop)
|
|
317
|
+
return null;
|
|
318
|
+
const resolve = pop + 0.18;
|
|
319
|
+
const settled = settledCell(context);
|
|
320
|
+
if (context.progress <= resolve) {
|
|
321
|
+
const tick = Math.floor(context.progress * context.duration / 4);
|
|
322
|
+
const shades = ['░', '▒', '▓'];
|
|
323
|
+
return { glyph: shades[Math.floor(context.rnd(tick * 3 + 2) * 3)], color: mix(context.base, '#1e1f1c', 0.55 + context.rnd(5) * 0.25) };
|
|
324
|
+
}
|
|
325
|
+
const flash = Math.max(0, 1 - (context.progress - resolve) / 0.08);
|
|
326
|
+
const color = flash > 0 ? mix(settled.color, '#f8f8f2', flash * 0.85) : settled.color;
|
|
327
|
+
return { glyph: context.glyph, color };
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
// kanagawa — a sumi-e wash: a wide soft brush draws the mark in three
|
|
331
|
+
// passes (top, middle, bottom bands), each stroke wet and dark at the
|
|
332
|
+
// front, drying lighter toward the tail.
|
|
333
|
+
kanagawa: {
|
|
334
|
+
cell(context) {
|
|
335
|
+
const band = Math.floor(context.rowIndex / 2);
|
|
336
|
+
const strokeFront = (context.progress * 1.15 - band * 0.22) * (context.span + 8) - 4;
|
|
337
|
+
if (context.diagonal > strokeFront)
|
|
338
|
+
return null;
|
|
339
|
+
const dx = context.diagonal - strokeFront;
|
|
340
|
+
const wet = Math.exp(-(dx * dx) / 26);
|
|
341
|
+
const settled = settledCell(context);
|
|
342
|
+
const color = wet > 0.02 ? mix(settled.color, '#1f1f28', Math.min(0.5, wet * 0.5)) : settled.color;
|
|
343
|
+
return { glyph: context.glyph, color };
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
// everforest — fog lift: dense pale fog envelops nothing yet — the fog
|
|
347
|
+
// edge retreats to the left as time passes, and cells emerge in its wake
|
|
348
|
+
// from the right. Each cell brightens as the haze burns off.
|
|
349
|
+
everforest: {
|
|
350
|
+
cell(context) {
|
|
351
|
+
const drift = context.progress * (context.span + 16) - 8;
|
|
352
|
+
const wisp = (context.rnd(6) - 0.5) * 6 + context.rowIndex * 1.7;
|
|
353
|
+
if (context.diagonal > drift + wisp)
|
|
354
|
+
return null;
|
|
355
|
+
const thin = Math.max(0, 1 - (drift + wisp - context.diagonal) / 8);
|
|
356
|
+
const settled = settledCell(context);
|
|
357
|
+
const color = thin > 0.02 ? mix(settled.color, '#d3c6aa', thin * 0.5) : settled.color;
|
|
358
|
+
return { glyph: context.glyph, color };
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
// synthwave — VHS tear-in: the mark materializes out of tracking noise,
|
|
362
|
+
// rows tearing in top to bottom with chromatic aberration, stabilizing
|
|
363
|
+
// row by row until the tape locks.
|
|
364
|
+
synthwave: {
|
|
365
|
+
cell(context) {
|
|
366
|
+
const rowLock = 0.06 + (context.rowIndex / 6) * 0.6;
|
|
367
|
+
if (context.progress < rowLock)
|
|
368
|
+
return null;
|
|
369
|
+
const tear = Math.max(0, 1 - (context.progress - rowLock) / 0.14);
|
|
370
|
+
if (tear > 0 && context.rnd(Math.floor(context.progress * context.duration) * 17 + context.rowIndex * 41) < tear * 0.5)
|
|
371
|
+
return null;
|
|
372
|
+
const settled = settledCell(context);
|
|
373
|
+
const chroma = tear > 0 ? tear * 0.6 : 0;
|
|
374
|
+
const color = chroma > 0 ? mix(settled.color, chroma * 2 > 1 ? '#36f9f6' : '#fe4450', chroma) : settled.color;
|
|
375
|
+
return { glyph: context.glyph, color };
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
// matrix — rain condensation: glyph rain falls through the mark's bounding
|
|
379
|
+
// box and the letterforms condense where the rain strikes, as if the code
|
|
380
|
+
// itself is condensing out of the downpour.
|
|
381
|
+
matrix: {
|
|
382
|
+
cell(context) {
|
|
383
|
+
const strike = 0.06 + context.rnd(8) * 0.6;
|
|
384
|
+
if (context.progress < strike)
|
|
385
|
+
return null;
|
|
386
|
+
const settle = Math.max(0, 1 - (context.progress - strike) / 0.1);
|
|
387
|
+
const settled = settledCell(context);
|
|
388
|
+
const color = settle > 0 ? mix(settled.color, '#c8ffd9', settle * 0.7) : settled.color;
|
|
389
|
+
return { glyph: context.glyph, color };
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
// solarized-light — blueprint develop: a scanning bar sweeps down over
|
|
393
|
+
// blank paper; behind it the ink darkens rapidly from nothing into the
|
|
394
|
+
// finished solarized-light palette.
|
|
395
|
+
'solarized-light': {
|
|
396
|
+
cell(context) {
|
|
397
|
+
const scan = -1.5 + context.progress * 9;
|
|
398
|
+
if (context.rowIndex > scan)
|
|
399
|
+
return null;
|
|
400
|
+
const fresh = Math.max(0, 1 - (context.rowIndex - scan) / 1.4);
|
|
401
|
+
const settled = settledCell(context);
|
|
402
|
+
const color = fresh > 0.02 ? mix(settled.color, '#268bd2', fresh * 0.6) : settled.color;
|
|
403
|
+
return { glyph: context.glyph, color };
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
// github-light — skeleton shimmer: a pale wireframe flickers in for a
|
|
407
|
+
// couple of beats, then the fill floods in left to right with a crisp
|
|
408
|
+
// leading edge, converting the skeleton in place — nothing is ever
|
|
409
|
+
// erased — ending at full contrast.
|
|
410
|
+
'github-light': {
|
|
411
|
+
cell(context) {
|
|
412
|
+
const settled = settledCell(context);
|
|
413
|
+
const skeleton = { glyph: context.glyph, color: mix('#f6f8fa', '#d0d7de', 0.4) };
|
|
414
|
+
if (context.progress < 0.02)
|
|
415
|
+
return null;
|
|
416
|
+
if (context.progress < 0.28) {
|
|
417
|
+
// The wireframe breathes: bright beats and dim beats, but a cell
|
|
418
|
+
// stays painted once it has first appeared.
|
|
419
|
+
const sputter = context.rnd(Math.floor(context.progress * 100) * 29 + context.rowIndex * 17 + context.column);
|
|
420
|
+
if (sputter < 0.45)
|
|
421
|
+
return { glyph: context.glyph, color: mix('#f6f8fa', '#d0d7de', 0.75) };
|
|
422
|
+
return skeleton;
|
|
423
|
+
}
|
|
424
|
+
const front = (context.progress - 0.28) / 0.72 * (context.span + 6) - 3;
|
|
425
|
+
if (context.diagonal > front)
|
|
426
|
+
return skeleton; // not yet flooded: skeleton holds
|
|
427
|
+
const fresh = Math.max(0, 1 - (front - context.diagonal) / 4);
|
|
428
|
+
const color = fresh > 0.02 ? mix(settled.color, '#0969da', fresh * 0.5) : settled.color;
|
|
429
|
+
return { glyph: context.glyph, color };
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
41
433
|
export function renderWelcome(width, height, terminalHeight = height, frame = 0) {
|
|
42
434
|
width = Math.max(1, Math.floor(width));
|
|
43
435
|
height = Math.max(1, Math.floor(height));
|
|
@@ -47,18 +439,11 @@ export function renderWelcome(width, height, terminalHeight = height, frame = 0)
|
|
|
47
439
|
const cycle = frame % 80;
|
|
48
440
|
const logoTop = center - 3;
|
|
49
441
|
if (width >= LOGO_WIDTH + 3 && logoTop >= 0 && logoTop + LOGO.length <= height) {
|
|
50
|
-
// One clean motion layer for every theme: the shine rides diagonal "/"
|
|
51
|
-
// stripes (distance along u = x + rise · y) and ping-pongs end to end.
|
|
52
|
-
// Its phase advances at a rate that itself sways, so the band lingers at
|
|
53
|
-
// the ends, then dashes across — never a flat metronome. No other motion
|
|
54
|
-
// touches the letterforms.
|
|
55
442
|
const rise = 2;
|
|
56
443
|
const span = LOGO_WIDTH + rise * (LOGO.length - 1);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const bandCenter =
|
|
60
|
-
const bandWidth = 18;
|
|
61
|
-
const glowBoost = 0.7;
|
|
444
|
+
// Loop phase: the shine ping-pongs with a swaying rate, re-entering from
|
|
445
|
+
// the left each cycle, so the band lingers at the ends then dashes across.
|
|
446
|
+
const bandCenter = shineBandCenter(frame, span);
|
|
62
447
|
// The phosphor theme swaps the empty space around the mark for glyph
|
|
63
448
|
// rain: per-column heads fall the full height of the screen, trailing a
|
|
64
449
|
// fading wake. The letterforms themselves stay pristine on top.
|
|
@@ -99,21 +484,47 @@ export function renderWelcome(width, height, terminalHeight = height, frame = 0)
|
|
|
99
484
|
return pad;
|
|
100
485
|
};
|
|
101
486
|
const leftCells = Math.floor((width - LOGO_WIDTH) / 2);
|
|
487
|
+
const themeName = activeTuiTheme().name;
|
|
488
|
+
const intro = intros[themeName];
|
|
102
489
|
LOGO.forEach((template, rowIndex) => {
|
|
103
490
|
const row = logoTop + rowIndex;
|
|
104
491
|
let line = '';
|
|
105
492
|
for (let column = 0; column < LOGO_WIDTH; column++) {
|
|
106
493
|
const diagonal = column + rowIndex * rise;
|
|
107
|
-
const dx = diagonal - bandCenter;
|
|
108
|
-
const glow = Math.exp(-(dx * dx) / bandWidth);
|
|
109
|
-
const base = mix(palette.accent, palette.heading, diagonal / span);
|
|
110
|
-
const intensity = Math.min(0.95, glow * glowBoost);
|
|
111
|
-
const color = intensity > 0.03 ? mix(base, palette.headingStrong, intensity) : base;
|
|
112
494
|
const glyph = template[column];
|
|
113
495
|
if (glyph === ' ') {
|
|
114
496
|
line += rainAt(row, leftCells + column);
|
|
115
497
|
continue;
|
|
116
498
|
}
|
|
499
|
+
const base = mix(palette.accent, palette.heading, diagonal / span);
|
|
500
|
+
if (intro && introActive(frame)) {
|
|
501
|
+
const context = {
|
|
502
|
+
frame,
|
|
503
|
+
duration: INTRO_DURATION,
|
|
504
|
+
progress: frame / INTRO_DURATION,
|
|
505
|
+
rowIndex,
|
|
506
|
+
column,
|
|
507
|
+
diagonal,
|
|
508
|
+
span,
|
|
509
|
+
width: LOGO_WIDTH,
|
|
510
|
+
glyph,
|
|
511
|
+
base,
|
|
512
|
+
accent: palette.accent,
|
|
513
|
+
heading: palette.heading,
|
|
514
|
+
headingStrong: palette.headingStrong,
|
|
515
|
+
text: palette.text,
|
|
516
|
+
rnd: (seed) => hash01(seed * 7919 + rowIndex * 131 + column * 7 + 5),
|
|
517
|
+
};
|
|
518
|
+
const painted = intro.cell(context);
|
|
519
|
+
if (painted)
|
|
520
|
+
line += `{${painted.color}-fg}${painted.glyph}{/${painted.color}-fg}`;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Loop: the settled gradient with the shine riding on top.
|
|
524
|
+
const dx = diagonal - bandCenter;
|
|
525
|
+
const glow = Math.exp(-(dx * dx) / SHINE_BAND_WIDTH);
|
|
526
|
+
const intensity = Math.min(0.95, glow * SHINE_GLOW_BOOST);
|
|
527
|
+
const color = intensity > 0.03 ? mix(base, palette.headingStrong, intensity) : base;
|
|
117
528
|
line += `{${color}-fg}${glyph}{/${color}-fg}`;
|
|
118
529
|
}
|
|
119
530
|
rows[row] = `${rainPad(row, 0, leftCells)}${line}${rainPad(row, leftCells + LOGO_WIDTH, width)}`;
|
|
@@ -121,7 +532,7 @@ export function renderWelcome(width, height, terminalHeight = height, frame = 0)
|
|
|
121
532
|
const brandRow = logoTop + LOGO.length + 1;
|
|
122
533
|
if (brandRow < height && BRAND.length <= width) {
|
|
123
534
|
const brandLeft = Math.floor((width - BRAND.length) / 2);
|
|
124
|
-
const brand = `{${palette.headingStrong}-fg}{bold}
|
|
535
|
+
const brand = `{${palette.headingStrong}-fg}{bold}{/bold}{/${palette.headingStrong}-fg}{white-fg}{bold}${BRAND}{/bold}{/white-fg}`;
|
|
125
536
|
rows[brandRow] = `${rainPad(brandRow, 0, brandLeft)}${brand}${rainPad(brandRow, brandLeft + BRAND.length, width)}`;
|
|
126
537
|
}
|
|
127
538
|
const taglineRow = brandRow + 1;
|