@stevezhou/sisu 0.1.5 → 0.1.6

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/dist/logo.js CHANGED
@@ -4,6 +4,8 @@ exports.sisuTreeLines = exports.sisuTreeArt = exports.sisuMarkLines = exports.si
4
4
  exports.sisuMobiusArt = sisuMobiusArt;
5
5
  exports.sisuWordmark = sisuWordmark;
6
6
  exports.displayWidth = displayWidth;
7
+ exports.sisuSplashFrame = sisuSplashFrame;
8
+ exports.sisuSplashHeight = sisuSplashHeight;
7
9
  exports.sisuSplash = sisuSplash;
8
10
  exports.sisuWelcomeCopy = sisuWelcomeCopy;
9
11
  exports.sisuBanner = sisuBanner;
@@ -115,9 +117,9 @@ function copyColumn(columns, color, compact) {
115
117
  }
116
118
  return lines;
117
119
  }
118
- function treeColumn(columns, color) {
119
- const raw = (0, tree_2.sisuTreeLines)(columns);
120
- const painted = (0, tree_2.sisuTreeArt)(columns, color).split('\n');
120
+ function treeColumn(columns, color, phase, grow) {
121
+ const raw = (0, tree_2.sisuTreeLines)(columns, phase, grow);
122
+ const painted = (0, tree_2.sisuTreeArt)(columns, color, phase, grow).split('\n');
121
123
  const treeWidth = raw.reduce((max, line) => Math.max(max, line.length), 0);
122
124
  const pad = Math.max(0, Math.floor((columns - treeWidth) / 2));
123
125
  return painted.map((line, i) => {
@@ -125,31 +127,45 @@ function treeColumn(columns, color) {
125
127
  return padLeft(`${line}${' '.repeat(Math.max(0, treeWidth - vis))}`, pad);
126
128
  });
127
129
  }
130
+ const STILL_PHASE = 2.05;
128
131
  /**
129
- * Login-page splash: lockup + 思有所溯 + 题跋 over the 溯源之树.
130
- * Side-by-side on a wide terminal; stacked when the sheet is narrow.
132
+ * Login-page splash. `phase` orbits the camera; `grow` is 0..1 paint progress.
133
+ * Line count is stable for a given width so the intro can rewrite in place.
131
134
  */
132
- function sisuSplash(columns = 80, color = true) {
135
+ function sisuSplashFrame(columns = 80, color = true, phase = STILL_PHASE, grow = 1) {
133
136
  const width = Math.max(20, Math.floor(columns));
134
137
  const compact = width < 52;
135
138
  const copy = copyColumn(width, color, compact);
136
- const tree = treeColumn(width, color);
137
- if (width >= 100) {
139
+ if (width >= 72) {
138
140
  const gap = 3;
139
- const leftW = Math.max(...copy.map((line) => displayWidth(line)));
140
- const rightW = width - leftW - gap;
141
- const right = treeColumn(rightW, color);
142
- const rows = Math.max(copy.length, right.length);
143
- const merged = [''];
141
+ const leftBudget = Math.min(44, Math.floor(width * 0.42));
142
+ const leftCopy = copyColumn(leftBudget, color, true);
143
+ const leftW = Math.max(...leftCopy.map((line) => displayWidth(line)), 22);
144
+ const rightW = Math.max(28, width - leftW - gap);
145
+ const right = treeColumn(rightW, color, phase, grow);
146
+ const rows = Math.max(leftCopy.length, right.length);
147
+ const merged = [];
144
148
  for (let i = 0; i < rows; i += 1) {
145
- const left = copy[i] ?? '';
149
+ const left = leftCopy[i] ?? '';
146
150
  const pad = Math.max(0, leftW + gap - displayWidth(left));
147
151
  merged.push(`${left}${' '.repeat(pad)}${right[i] ?? ''}`);
148
152
  }
149
- merged.push('');
153
+ const notes = colophon(width, color);
154
+ if (notes.length) {
155
+ merged.push('');
156
+ for (const note of notes)
157
+ merged.push(padLeft(note, 2));
158
+ }
150
159
  return merged.join('\n');
151
160
  }
152
- return ['', ...copy, '', ...tree, ''].join('\n');
161
+ const tree = treeColumn(width, color, phase, grow);
162
+ return [...copy, '', ...tree].join('\n');
163
+ }
164
+ function sisuSplashHeight(columns = 80) {
165
+ return sisuSplashFrame(columns, false).split('\n').length;
166
+ }
167
+ function sisuSplash(columns = 80, color = true) {
168
+ return sisuSplashFrame(columns, color, STILL_PHASE, 1);
153
169
  }
154
170
  function sisuWelcomeCopy(guest, color = true) {
155
171
  const lines = [
@@ -165,8 +181,8 @@ function sisuWelcomeCopy(guest, color = true) {
165
181
  }
166
182
  return lines;
167
183
  }
168
- function sisuBanner(columns = 80, _phase = 0, color = false) {
169
- return sisuSplash(columns, color);
184
+ function sisuBanner(columns = 80, phase = STILL_PHASE, color = false) {
185
+ return sisuSplashFrame(columns, color, phase, 1);
170
186
  }
171
187
  function stripAnsi(text) {
172
188
  return text.replace(/\x1b\[[0-9;]*m/g, '');
package/dist/main.js CHANGED
@@ -28,7 +28,7 @@ Usage:
28
28
  sisu history
29
29
  sisu thread <conversation-id>
30
30
  sisu training --on|--off
31
- sisu interactive TUI (welcome tree)
31
+ sisu interactive TUI (3D welcome tree)
32
32
  sisu help
33
33
 
34
34
  Auth and workspaces live in $SISU_HOME (default ~/.sisu), shared with Desktop.
package/dist/tree.js CHANGED
@@ -1,116 +1,331 @@
1
1
  "use strict";
2
- /** 溯源之树 hand-inked ASCII of the login welcome painting. */
2
+ /** Volumetric 溯源之树: 3D L-system, z-buffer, lighting, grow + orbit. */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.buildTree3d = buildTree3d;
5
+ exports.treeFrameHeight = treeFrameHeight;
6
+ exports.treeFrameWidth = treeFrameWidth;
7
+ exports.renderTreeFrame = renderTreeFrame;
4
8
  exports.sisuTreeLines = sisuTreeLines;
5
9
  exports.sisuTreeArt = sisuTreeArt;
6
10
  exports.sisuTreeWidth = sisuTreeWidth;
7
11
  exports.sisuTreeHeight = sisuTreeHeight;
8
12
  const RESET = '\x1b[0m';
9
- const INK = [196, 186, 170];
10
- const INK_DIM = [130, 120, 108];
11
- const FRUIT = [184, 90, 58];
12
- const HILL = [90, 82, 70];
13
- /**
14
- * Desktop login tree. Trunk sits near center; crown opens into an airy
15
- * fan with terracotta tips — the same 图景 as BrandCanvas, not a Möbius.
16
- */
17
- const TREE_WIDE = [
18
- ' • • • • • • •',
19
- ' • • • • • • • • • •',
20
- ' • • • • • • • • • • • •',
21
- ' • • ╲ • ╲ • ╱ • ╲ • • •',
22
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╲ ╱',
23
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╲╱',
24
- ' ╲ ╲╱ ╲ ╲╱ ╲ ╱ ╲ ╲ ╱',
25
- ' ╲ ╱ ╲ ╱ Y ╲ ╲ ╱',
26
- ' ╲ ╱ ╲ ╱ ╱ ╲ ╲╱',
27
- ' ╲ ╱ Y ╱ ╲ ╱',
28
- ' ╲ ╱ ╱ ╲ ╱ ╲╱',
29
- ' Y ╱ ╲ ╱ ╱',
30
- ' │ ╱ ╲ ╱',
31
- ' │ ╱ Y',
32
- ' │ ╱',
33
- ' │',
34
- '▁▁▁▂▂▁▁▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
35
- ];
36
- const TREE_MID = [
37
- ' • • • • • • •',
38
- ' • • • • • • • • •',
39
- ' • • ╲ • ╲ • ╱ • ╲ • •',
40
- ' • ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ ╲ ╱',
41
- ' ╲ ╲ ╱ ╲ ╲ ╱ ╲ ╱ ╲ Y',
42
- ' ╲ Y ╲ Y ╲╱ ╲ ╱',
43
- ' ╲ ╱ ╲ ╱ ╱ Y',
44
- ' ╲╱ Y ╱ ╱',
45
- ' │ ╱ ╲ ╱',
46
- ' │ ╱ ╲ ╱',
47
- ' │ ╱ ╲╱',
48
- ' │',
49
- '▁▁▂▂▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
50
- ];
51
- const TREE_NARROW = [
52
- ' • • • • •',
53
- ' • ╲ • ╱ • ╲ •',
54
- ' ╲ ╱ ╲ ╱ ╲ ╱ ╲╱',
55
- ' ╲ ╱ Y ╲╱ ╱',
56
- ' Y ╱ ╲ ╱',
57
- ' │ ╱ ╲╱',
58
- ' │',
59
- '▁▁▁▁▁▁▁▁│▁▁▁▁▁▁▁▁▁▁▁▁▁▁',
60
- ];
13
+ const SHADE = ' .:-=+*#%@';
14
+ const CELL_ASPECT = 0.52;
15
+ const CAM = 5.35;
16
+ const FOCAL = 4.7;
17
+ const LOOK_Y = 0.72;
18
+ const KEY = norm([-0.62, 0.72, 0.32]);
19
+ const FILL = norm([0.55, 0.18, 0.82]);
20
+ const VIEW = [0, 0.08, 1];
21
+ const INK = [198, 186, 168];
22
+ const FRUIT_RGB = [184, 90, 58];
23
+ const HILL_RGB = [92, 84, 72];
24
+ const TREE_SEED = 20260817;
25
+ const MAX_DEPTH = 5;
26
+ function clamp(value, lo, hi) {
27
+ return Math.max(lo, Math.min(hi, value));
28
+ }
61
29
  function lerp(a, b, t) {
62
30
  return a + (b - a) * t;
63
31
  }
64
- function paintRgb(ch, rgb) {
65
- return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${ch}`;
32
+ function norm(v) {
33
+ const length = Math.hypot(v[0], v[1], v[2]) || 1;
34
+ return [v[0] / length, v[1] / length, v[2] / length];
66
35
  }
67
- function inkAt(x, width) {
68
- const t = width <= 1 ? 0 : x / (width - 1);
36
+ function cross(a, b) {
69
37
  return [
70
- Math.round(lerp(INK[0], INK_DIM[0], t * 0.45)),
71
- Math.round(lerp(INK[1], INK_DIM[1], t * 0.45)),
72
- Math.round(lerp(INK[2], INK_DIM[2], t * 0.45)),
38
+ a[1] * b[2] - a[2] * b[1],
39
+ a[2] * b[0] - a[0] * b[2],
40
+ a[0] * b[1] - a[1] * b[2],
73
41
  ];
74
42
  }
75
- function paintLine(line, color) {
76
- if (!color)
77
- return line;
78
- let out = '';
79
- for (let i = 0; i < line.length; i += 1) {
80
- const ch = line[i];
81
- if (ch === ' ') {
82
- out += ' ';
83
- continue;
43
+ function dot(a, b) {
44
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
45
+ }
46
+ function add(a, b, s = 1) {
47
+ return [a[0] + b[0] * s, a[1] + b[1] * s, a[2] + b[2] * s];
48
+ }
49
+ function rotateY(x, z, angle) {
50
+ const c = Math.cos(angle);
51
+ const s = Math.sin(angle);
52
+ return [x * c + z * s, -x * s + z * c];
53
+ }
54
+ function rotateX(y, z, angle) {
55
+ const c = Math.cos(angle);
56
+ const s = Math.sin(angle);
57
+ return [y * c - z * s, y * s + z * c];
58
+ }
59
+ function mulberry32(seed) {
60
+ let a = seed >>> 0;
61
+ return () => {
62
+ a |= 0;
63
+ a = (a + 0x6d2b79f5) | 0;
64
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
65
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
66
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
67
+ };
68
+ }
69
+ function perpFrame(dir) {
70
+ const helper = Math.abs(dir[1]) < 0.86 ? [0, 1, 0] : [1, 0, 0];
71
+ const n1 = norm(cross(dir, helper));
72
+ const n2 = norm(cross(dir, n1));
73
+ return [n1, n2];
74
+ }
75
+ let cached = null;
76
+ function headingFrom(yaw, pitch) {
77
+ const sp = Math.sin(pitch);
78
+ return norm([sp * Math.sin(yaw), Math.cos(pitch), sp * Math.cos(yaw)]);
79
+ }
80
+ function buildTree3d(seed = TREE_SEED) {
81
+ if (cached && cached.seed === seed)
82
+ return cached.branches;
83
+ const rng = mulberry32(seed);
84
+ const branches = [];
85
+ const grow = (origin, yaw, pitch, len, radius, depth, t0) => {
86
+ if (depth > MAX_DEPTH || len < 0.08 || branches.length > 150)
87
+ return;
88
+ const heading = headingFrom(yaw, pitch);
89
+ const end = add(origin, heading, len);
90
+ const t1 = Math.min(1, t0 + len / 2.8);
91
+ const childCount = depth >= MAX_DEPTH ? 0 : rng() < 0.74 ? 2 : 3;
92
+ branches.push({
93
+ ax: origin[0],
94
+ ay: origin[1],
95
+ az: origin[2],
96
+ bx: end[0],
97
+ by: end[1],
98
+ bz: end[2],
99
+ radius,
100
+ depth,
101
+ t0,
102
+ t1,
103
+ tip: childCount === 0,
104
+ });
105
+ const baseYaw = yaw + (rng() - 0.5) * 0.4;
106
+ for (let i = 0; i < childCount; i += 1) {
107
+ const fork = ((i - (childCount - 1) / 2) * 2 * Math.PI) / childCount;
108
+ const childYaw = baseYaw + fork + (rng() - 0.5) * 0.35;
109
+ const childPitch = Math.min(0.98, pitch + (depth === 0 ? 0.4 : 0.26) + rng() * 0.22);
110
+ grow(end, childYaw, childPitch, len * (0.66 + rng() * 0.12), radius * 0.67, depth + 1, t1 + rng() * 0.02);
84
111
  }
85
- if (ch === '•') {
86
- out += paintRgb(ch, FRUIT);
87
- continue;
112
+ };
113
+ grow([0, 0, 0], 0.1, 0.03, 0.82, 0.095, 0, 0);
114
+ const tMax = branches.reduce((m, b) => Math.max(m, b.t1), 0) || 1;
115
+ for (const b of branches) {
116
+ b.t0 /= tMax;
117
+ b.t1 /= tMax;
118
+ }
119
+ cached = { seed, branches };
120
+ return branches;
121
+ }
122
+ function shadeChar(shade) {
123
+ return SHADE[Math.round(clamp(shade, 0, 1) * (SHADE.length - 1))] || '@';
124
+ }
125
+ function ansiRgb(r, g, b) {
126
+ return `\x1b[38;2;${r};${g};${b}m`;
127
+ }
128
+ function plot(grid, col, row, depth, shade, spec, kind) {
129
+ if (col < 0 || row < 0 || row >= grid.length || col >= grid[0].length)
130
+ return;
131
+ if (!Number.isFinite(col) || !Number.isFinite(row) || !Number.isFinite(depth))
132
+ return;
133
+ const prev = grid[row][col];
134
+ if (!prev || depth < prev.z) {
135
+ grid[row][col] = { z: depth, shade: clamp(shade, 0.05, 1), spec, kind };
136
+ }
137
+ }
138
+ function project(x, y, z, yaw, pitch, cols, rows) {
139
+ ;
140
+ [x, z] = rotateY(x, z, yaw);
141
+ [y, z] = rotateX(y - LOOK_Y, z, pitch);
142
+ const depth = CAM - z;
143
+ if (depth < 0.45 || !Number.isFinite(depth))
144
+ return null;
145
+ const px = (x * FOCAL) / depth;
146
+ const py = (y * FOCAL) / depth;
147
+ if (!Number.isFinite(px) || !Number.isFinite(py))
148
+ return null;
149
+ const scale = Math.min((cols * 0.9) / 2.05, (rows * 0.88) / (1.85 * CELL_ASPECT));
150
+ const col = Math.round(px * scale + (cols - 1) / 2);
151
+ const row = Math.round(-py * scale * CELL_ASPECT + rows * 0.54);
152
+ return { col, row, depth };
153
+ }
154
+ function light(normal, back = false) {
155
+ const facing = dot(normal, VIEW);
156
+ if (facing < 0) {
157
+ normal = [-normal[0], -normal[1], -normal[2]];
158
+ back = true;
159
+ }
160
+ const lambert = Math.max(0, dot(normal, KEY));
161
+ const fill = Math.max(0, dot(normal, FILL)) * 0.22;
162
+ const cam = Math.max(0, dot(normal, VIEW)) * 0.2;
163
+ const half = norm([KEY[0] + VIEW[0], KEY[1] + VIEW[1], KEY[2] + VIEW[2]]);
164
+ const spec = Math.pow(Math.max(0, dot(normal, half)), 22);
165
+ const rim = Math.pow(1 - Math.abs(dot(normal, VIEW)), 2.1) * 0.22;
166
+ const ambient = back ? 0.1 : 0.2;
167
+ const lit = ambient + lambert * (back ? 0.28 : 0.72) + fill + cam + spec * 0.45 + rim;
168
+ return { shade: clamp(Math.pow(lit, 1.15), 0.07, 1), spec };
169
+ }
170
+ function swayAt(x, y, z, phase) {
171
+ const h = clamp(y / 2.3, 0, 1);
172
+ const gust = Math.sin(phase * 1.7 + x * 1.4 + z * 0.8);
173
+ const lean = 0.11 * h * h * gust;
174
+ return [x + lean, y, z + lean * 0.35];
175
+ }
176
+ function sampleBranch(grid, branch, grow, phase, yaw, pitch, cols, rows) {
177
+ if (grow <= branch.t0)
178
+ return;
179
+ const visible = clamp((grow - branch.t0) / Math.max(0.02, branch.t1 - branch.t0), 0, 1);
180
+ const along = Math.max(4, Math.ceil((branch.depth <= 1 ? 24 : branch.depth <= 3 ? 12 : 6) * visible));
181
+ const rings = branch.depth <= 1 ? 9 : branch.depth <= 3 ? 4 : 2;
182
+ const dx = branch.bx - branch.ax;
183
+ const dy = branch.by - branch.ay;
184
+ const dz = branch.bz - branch.az;
185
+ const dir = norm([dx, dy, dz]);
186
+ const [n1, n2] = perpFrame(dir);
187
+ for (let i = 0; i <= along; i += 1) {
188
+ const t = (i / along) * visible;
189
+ const px = branch.ax + dx * t;
190
+ const py = branch.ay + dy * t;
191
+ const pz = branch.az + dz * t;
192
+ const radius = branch.radius * (1 - t * 0.28);
193
+ for (let r = 0; r < rings; r += 1) {
194
+ const a = (r / rings) * Math.PI * 2 + phase * 0.05;
195
+ const ox = n1[0] * Math.cos(a) + n2[0] * Math.sin(a);
196
+ const oy = n1[1] * Math.cos(a) + n2[1] * Math.sin(a);
197
+ const oz = n1[2] * Math.cos(a) + n2[2] * Math.sin(a);
198
+ const [sx, sy, sz] = swayAt(px + ox * radius, py + oy * radius, pz + oz * radius, phase);
199
+ let nx = ox;
200
+ let ny = oy;
201
+ let nz = oz;
202
+ [nx, nz] = rotateY(nx, nz, yaw);
203
+ [ny, nz] = rotateX(ny, nz, pitch);
204
+ const hit = project(sx, sy, sz, yaw, pitch, cols, rows);
205
+ if (!hit)
206
+ continue;
207
+ const lit = light(norm([nx, ny, nz]));
208
+ const fog = 0.45 + 0.55 * clamp(1 - (hit.depth - 5.2) / 4.2, 0, 1);
209
+ plot(grid, hit.col, hit.row, hit.depth, lit.shade * fog, lit.spec * fog, 'wood');
88
210
  }
89
- if (ch === '▁' || ch === '▂') {
90
- out += paintRgb(ch, HILL);
211
+ }
212
+ }
213
+ function sampleFruit(grid, branches, grow, phase, yaw, pitch, cols, rows) {
214
+ const placed = [];
215
+ const gap = 0.28;
216
+ let count = 0;
217
+ for (const branch of branches) {
218
+ if (!branch.tip || grow < branch.t1)
219
+ continue;
220
+ if (placed.some((p) => Math.hypot(p[0] - branch.bx, p[1] - branch.by, p[2] - branch.bz) < gap))
91
221
  continue;
222
+ placed.push([branch.bx, branch.by, branch.bz]);
223
+ const [bx, by, bz] = swayAt(branch.bx, branch.by, branch.bz, phase);
224
+ const hit = project(bx, by, bz, yaw, pitch, cols, rows);
225
+ if (!hit)
226
+ continue;
227
+ const twinkle = 0.88 + 0.12 * Math.sin(phase * 3.1 + branch.bx * 7);
228
+ plot(grid, hit.col, hit.row, hit.depth - 0.02, 0.92 * twinkle, 0.55, 'fruit');
229
+ count += 1;
230
+ if (count >= 22)
231
+ break;
232
+ }
233
+ }
234
+ function sampleGround(grid, phase, yaw, pitch, cols, rows) {
235
+ for (let ix = -16; ix <= 16; ix += 1) {
236
+ for (let iz = -14; iz <= 14; iz += 1) {
237
+ const x = ix * 0.12;
238
+ const z = iz * 0.12;
239
+ const rr = (x * x) / (1.85 * 1.85) + (z * z) / (1.45 * 1.45);
240
+ if (rr > 1)
241
+ continue;
242
+ if (rr < 0.88 && rr > 0.18 && (ix * 3 + iz * 5) % 4 !== 0)
243
+ continue;
244
+ const y = 0.05 * Math.sin(x * 1.6 + 0.5) * Math.cos(z * 1.25) +
245
+ 0.018 * Math.sin(x * 3.0 + z * 2.1 + phase * 0.15);
246
+ const hit = project(x, y, z, yaw, pitch, cols, rows);
247
+ if (!hit)
248
+ continue;
249
+ const nx = -0.08 * Math.cos(x * 1.6 + 0.5);
250
+ const nz = 0.06 * Math.sin(z * 1.25);
251
+ let nnx = nx;
252
+ let nny = 1;
253
+ let nnz = nz;
254
+ [nnx, nnz] = rotateY(nnx, nnz, yaw);
255
+ [nny, nnz] = rotateX(nny, nnz, pitch);
256
+ const lit = light(norm([nnx, nny, nnz]), true);
257
+ const fog = 0.22 + 0.35 * clamp(1 - rr, 0, 1) * clamp(1 - (hit.depth - 5.2) / 4.8, 0, 1);
258
+ plot(grid, hit.col, hit.row, hit.depth + 0.1, lit.shade * fog, 0, 'ground');
92
259
  }
93
- out += paintRgb(ch, inkAt(i, line.length));
94
260
  }
95
- return `${out}${RESET}`;
96
261
  }
97
- function padBlock(lines) {
98
- const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
99
- return lines.map((line) => line.padEnd(width, ' '));
262
+ function paintCell(cell, color) {
263
+ const ch = cell.kind === 'fruit' ? '\u2022' : shadeChar(cell.shade);
264
+ if (!color)
265
+ return ch;
266
+ const base = cell.kind === 'fruit' ? FRUIT_RGB : cell.kind === 'ground' ? HILL_RGB : INK;
267
+ const lift = 0.22 + cell.shade * 0.95 + cell.spec * 0.55;
268
+ const r = clamp(Math.round(base[0] * lift), 0, 255);
269
+ const g = clamp(Math.round(base[1] * lift), 0, 255);
270
+ const b = clamp(Math.round(base[2] * lift), 0, 255);
271
+ if (![r, g, b].every(Number.isFinite))
272
+ return ch;
273
+ return `${ansiRgb(r, g, b)}${ch}${RESET}`;
274
+ }
275
+ function treeFrameHeight(columns = 80) {
276
+ if (columns < 44)
277
+ return 10;
278
+ if (columns < 68)
279
+ return 14;
280
+ return 18;
281
+ }
282
+ function treeFrameWidth(columns = 80) {
283
+ if (columns < 44)
284
+ return Math.max(28, columns);
285
+ if (columns < 68)
286
+ return Math.min(52, columns);
287
+ return Math.min(72, Math.max(56, columns - 2));
288
+ }
289
+ function renderTreeFrame(options = {}) {
290
+ const cols = Math.max(24, Math.floor(options.cols ?? 64));
291
+ const rows = Math.max(8, Math.floor(options.rows ?? 16));
292
+ const phase = options.phase ?? 0.4;
293
+ const grow = clamp(options.grow ?? 1, 0, 1);
294
+ const color = options.color !== false;
295
+ const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
296
+ const yaw = 0.42 + 0.62 * Math.sin(phase);
297
+ const pitch = 0.3 + 0.07 * Math.cos(phase * 0.85);
298
+ const branches = buildTree3d();
299
+ sampleGround(grid, phase, yaw, pitch, cols, rows);
300
+ for (const branch of branches)
301
+ sampleBranch(grid, branch, grow, phase, yaw, pitch, cols, rows);
302
+ sampleFruit(grid, branches, grow, phase, yaw, pitch, cols, rows);
303
+ return grid
304
+ .map((line) => line.map((cell) => (cell ? paintCell(cell, color) : ' ')).join(''))
305
+ .join('\n');
100
306
  }
101
- function sisuTreeLines(columns = 80) {
102
- if (columns >= 68)
103
- return padBlock(TREE_WIDE);
104
- if (columns >= 44)
105
- return padBlock(TREE_MID);
106
- return padBlock(TREE_NARROW);
307
+ function sisuTreeLines(columns = 80, phase = 2.05, grow = 1) {
308
+ const art = renderTreeFrame({
309
+ cols: treeFrameWidth(columns),
310
+ rows: treeFrameHeight(columns),
311
+ phase,
312
+ grow,
313
+ color: false,
314
+ });
315
+ return art.split('\n');
107
316
  }
108
- function sisuTreeArt(columns = 80, color = true) {
109
- return sisuTreeLines(columns).map((line) => paintLine(line, color)).join('\n');
317
+ function sisuTreeArt(columns = 80, color = true, phase = 2.05, grow = 1) {
318
+ return renderTreeFrame({
319
+ cols: treeFrameWidth(columns),
320
+ rows: treeFrameHeight(columns),
321
+ phase,
322
+ grow,
323
+ color,
324
+ });
110
325
  }
111
326
  function sisuTreeWidth(columns = 80) {
112
- return sisuTreeLines(columns).reduce((max, line) => Math.max(max, line.length), 0);
327
+ return treeFrameWidth(columns);
113
328
  }
114
329
  function sisuTreeHeight(columns = 80) {
115
- return sisuTreeLines(columns).length;
330
+ return treeFrameHeight(columns);
116
331
  }
package/dist/tui.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.shouldAnimateSplash = shouldAnimateSplash;
7
7
  exports.playMobiusIntro = playMobiusIntro;
8
+ exports.playTreeIntro = playTreeIntro;
8
9
  exports.defaultTuiIo = defaultTuiIo;
9
10
  exports.tuiHelp = tuiHelp;
10
11
  exports.runTui = runTui;
@@ -48,6 +49,30 @@ async function playMobiusIntro(io, options = {}) {
48
49
  io.write('\x1b[?25h');
49
50
  io.write(`\n${(0, logo_1.sisuWordmark)()}\n\n`);
50
51
  }
52
+ function easeOutCubic(t) {
53
+ return 1 - (1 - t) ** 3;
54
+ }
55
+ /** Grow the 溯源之树, then orbit so lighting and occlusion read as volume. */
56
+ async function playTreeIntro(io, options = {}) {
57
+ const columns = options.columns ?? process.stdout.columns ?? 80;
58
+ const frames = Math.max(2, options.frames ?? 36);
59
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
60
+ const color = options.color ?? Boolean(process.stdout.isTTY);
61
+ const rows = (0, logo_1.sisuSplashHeight)(columns);
62
+ io.write('\x1b[?25l');
63
+ for (let i = 0; i < frames; i += 1) {
64
+ const u = i / (frames - 1);
65
+ const grow = easeOutCubic(Math.min(1, u / 0.58));
66
+ const phase = 0.12 + u * 2.35;
67
+ const art = (0, logo_1.sisuSplashFrame)(columns, color, phase, grow);
68
+ if (i === 0)
69
+ io.write(`${art}\n`);
70
+ else
71
+ io.write(`\x1b[${rows}A${art}\n`);
72
+ await sleep(42);
73
+ }
74
+ io.write('\x1b[?25h');
75
+ }
51
76
  function defaultTuiIo() {
52
77
  let rl;
53
78
  const ensureRl = () => {
@@ -174,9 +199,15 @@ async function runTui(io, deps = {}) {
174
199
  const columns = deps.columns ?? process.stdout.columns ?? 80;
175
200
  const animate = deps.animate ?? shouldAnimateSplash();
176
201
  try {
177
- io.write(`${(0, logo_1.sisuSplash)(columns, true)}\n`);
178
202
  if (animate) {
179
- await (deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(80);
203
+ await playTreeIntro(io, {
204
+ columns,
205
+ color: deps.color ?? true,
206
+ sleep: deps.sleep,
207
+ });
208
+ }
209
+ else {
210
+ io.write(`${(0, logo_1.sisuSplash)(columns, true)}\n`);
180
211
  }
181
212
  const account = auth();
182
213
  const startWebLogin = async (notify) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",