@stevezhou/sisu 0.1.4 → 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 +161 -10
- package/dist/main.js +1 -1
- package/dist/pager/render.js +14 -20
- package/dist/tree.js +331 -0
- package/dist/tui.js +33 -2
- package/package.json +1 -1
package/dist/logo.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sisuMarkLines = exports.sisuMarkArt = void 0;
|
|
3
|
+
exports.sisuTreeLines = exports.sisuTreeArt = exports.sisuMarkLines = exports.sisuMarkArt = void 0;
|
|
4
4
|
exports.sisuMobiusArt = sisuMobiusArt;
|
|
5
5
|
exports.sisuWordmark = sisuWordmark;
|
|
6
|
+
exports.displayWidth = displayWidth;
|
|
7
|
+
exports.sisuSplashFrame = sisuSplashFrame;
|
|
8
|
+
exports.sisuSplashHeight = sisuSplashHeight;
|
|
6
9
|
exports.sisuSplash = sisuSplash;
|
|
10
|
+
exports.sisuWelcomeCopy = sisuWelcomeCopy;
|
|
7
11
|
exports.sisuBanner = sisuBanner;
|
|
8
12
|
exports.stripAnsi = stripAnsi;
|
|
9
13
|
var mark_1 = require("./mark");
|
|
10
14
|
Object.defineProperty(exports, "sisuMarkArt", { enumerable: true, get: function () { return mark_1.sisuMarkArt; } });
|
|
11
15
|
Object.defineProperty(exports, "sisuMarkLines", { enumerable: true, get: function () { return mark_1.sisuMarkLines; } });
|
|
16
|
+
var tree_1 = require("./tree");
|
|
17
|
+
Object.defineProperty(exports, "sisuTreeArt", { enumerable: true, get: function () { return tree_1.sisuTreeArt; } });
|
|
18
|
+
Object.defineProperty(exports, "sisuTreeLines", { enumerable: true, get: function () { return tree_1.sisuTreeLines; } });
|
|
12
19
|
const mark_2 = require("./mark");
|
|
13
20
|
const mobius_1 = require("./mobius");
|
|
21
|
+
const tree_2 = require("./tree");
|
|
22
|
+
const RESET = '\x1b[0m';
|
|
23
|
+
const TERRACOTTA = '\x1b[38;2;184;90;58m';
|
|
24
|
+
const INK = '\x1b[38;2;220;214;204m';
|
|
25
|
+
const INK_DIM = '\x1b[38;2;140;132;120m';
|
|
26
|
+
const HEADLINE = '思有所溯';
|
|
27
|
+
const TAGLINE = '为科研而生的工作台——记录退到思考之后,不打扰任何一念。';
|
|
28
|
+
const COLOPHON = [
|
|
29
|
+
'对话、文献与数据,落笔成档',
|
|
30
|
+
'每个结论,都能沿枝回到它的根',
|
|
31
|
+
'众人的知识彼此嫁接,长成一片林',
|
|
32
|
+
];
|
|
33
|
+
const NUMERALS = ['一', '二', '三'];
|
|
14
34
|
function sisuMobiusArt(columns = 80, phase = 0, color = false) {
|
|
15
35
|
return (0, mobius_1.renderMobiusFrame)({
|
|
16
36
|
cols: (0, mobius_1.mobiusFrameWidth)(columns),
|
|
@@ -22,16 +42,147 @@ function sisuMobiusArt(columns = 80, phase = 0, color = false) {
|
|
|
22
42
|
function sisuWordmark() {
|
|
23
43
|
return ['思溯', 'SISU'].join('\n');
|
|
24
44
|
}
|
|
25
|
-
/**
|
|
45
|
+
/** Terminal cells, not JS string length — CJK and ∞ must sit on the grid. */
|
|
46
|
+
function displayWidth(text) {
|
|
47
|
+
let width = 0;
|
|
48
|
+
for (const ch of stripAnsi(text)) {
|
|
49
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
50
|
+
width += isWide(code) ? 2 : 1;
|
|
51
|
+
}
|
|
52
|
+
return width;
|
|
53
|
+
}
|
|
54
|
+
function isWide(code) {
|
|
55
|
+
return ((code >= 0x1100 && code <= 0x115f) ||
|
|
56
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
57
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
58
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
59
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
60
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
61
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
62
|
+
(code >= 0xffe0 && code <= 0xffe6));
|
|
63
|
+
}
|
|
64
|
+
function paint(text, prefix, color) {
|
|
65
|
+
if (!color || !text)
|
|
66
|
+
return text;
|
|
67
|
+
return `${prefix}${text}${RESET}`;
|
|
68
|
+
}
|
|
69
|
+
function infinityMark(color) {
|
|
70
|
+
if (!color)
|
|
71
|
+
return '∞';
|
|
72
|
+
const [r, g, b] = (0, mark_2.markRgb)(0, 2);
|
|
73
|
+
return `\x1b[38;2;${r};${g};${b}m∞${RESET}`;
|
|
74
|
+
}
|
|
75
|
+
function lockup(color) {
|
|
76
|
+
const inf = infinityMark(color);
|
|
77
|
+
const name = paint('思溯', INK, color);
|
|
78
|
+
const latin = paint('SiSu', INK_DIM, color);
|
|
79
|
+
return [`${inf} ${name}`, ` ${latin}`];
|
|
80
|
+
}
|
|
81
|
+
function colophon(columns, color) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
for (let i = 0; i < COLOPHON.length; i += 1) {
|
|
84
|
+
const num = paint(NUMERALS[i], TERRACOTTA, color);
|
|
85
|
+
const body = paint(COLOPHON[i], INK_DIM, color);
|
|
86
|
+
const line = `${num} ${body}`;
|
|
87
|
+
if (displayWidth(line) + 2 <= columns)
|
|
88
|
+
lines.push(line);
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
function padLeft(text, n) {
|
|
93
|
+
return `${' '.repeat(Math.max(0, n))}${text}`;
|
|
94
|
+
}
|
|
95
|
+
function center(text, width) {
|
|
96
|
+
const vis = displayWidth(text);
|
|
97
|
+
const pad = Math.max(0, Math.floor((width - vis) / 2));
|
|
98
|
+
return padLeft(text, pad);
|
|
99
|
+
}
|
|
100
|
+
function copyColumn(columns, color, compact) {
|
|
101
|
+
const indent = 2;
|
|
102
|
+
const lines = [
|
|
103
|
+
...lockup(color).map((line) => padLeft(line, indent)),
|
|
104
|
+
'',
|
|
105
|
+
padLeft(paint(HEADLINE, INK, color), indent),
|
|
106
|
+
];
|
|
107
|
+
if (!compact && displayWidth(TAGLINE) + indent <= columns) {
|
|
108
|
+
lines.push(padLeft(paint(TAGLINE, INK_DIM, color), indent));
|
|
109
|
+
}
|
|
110
|
+
if (!compact) {
|
|
111
|
+
const notes = colophon(columns, color);
|
|
112
|
+
if (notes.length) {
|
|
113
|
+
lines.push('');
|
|
114
|
+
for (const note of notes)
|
|
115
|
+
lines.push(padLeft(note, indent));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
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');
|
|
123
|
+
const treeWidth = raw.reduce((max, line) => Math.max(max, line.length), 0);
|
|
124
|
+
const pad = Math.max(0, Math.floor((columns - treeWidth) / 2));
|
|
125
|
+
return painted.map((line, i) => {
|
|
126
|
+
const vis = raw[i]?.length ?? displayWidth(line);
|
|
127
|
+
return padLeft(`${line}${' '.repeat(Math.max(0, treeWidth - vis))}`, pad);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const STILL_PHASE = 2.05;
|
|
131
|
+
/**
|
|
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.
|
|
134
|
+
*/
|
|
135
|
+
function sisuSplashFrame(columns = 80, color = true, phase = STILL_PHASE, grow = 1) {
|
|
136
|
+
const width = Math.max(20, Math.floor(columns));
|
|
137
|
+
const compact = width < 52;
|
|
138
|
+
const copy = copyColumn(width, color, compact);
|
|
139
|
+
if (width >= 72) {
|
|
140
|
+
const gap = 3;
|
|
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 = [];
|
|
148
|
+
for (let i = 0; i < rows; i += 1) {
|
|
149
|
+
const left = leftCopy[i] ?? '';
|
|
150
|
+
const pad = Math.max(0, leftW + gap - displayWidth(left));
|
|
151
|
+
merged.push(`${left}${' '.repeat(pad)}${right[i] ?? ''}`);
|
|
152
|
+
}
|
|
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
|
+
}
|
|
159
|
+
return merged.join('\n');
|
|
160
|
+
}
|
|
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
|
+
}
|
|
26
167
|
function sisuSplash(columns = 80, color = true) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
168
|
+
return sisuSplashFrame(columns, color, STILL_PHASE, 1);
|
|
169
|
+
}
|
|
170
|
+
function sisuWelcomeCopy(guest, color = true) {
|
|
171
|
+
const lines = [
|
|
172
|
+
...lockup(color),
|
|
173
|
+
paint(HEADLINE, INK, color),
|
|
174
|
+
];
|
|
175
|
+
if (guest) {
|
|
176
|
+
lines.push(paint('Sign in to start a conversation.', INK_DIM, color));
|
|
177
|
+
lines.push(paint('/login browser /help commands', INK_DIM, color));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
lines.push(paint('Ask anything, or type /help.', INK_DIM, color));
|
|
181
|
+
}
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
function sisuBanner(columns = 80, phase = STILL_PHASE, color = false) {
|
|
185
|
+
return sisuSplashFrame(columns, color, phase, 1);
|
|
35
186
|
}
|
|
36
187
|
function stripAnsi(text) {
|
|
37
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 (
|
|
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/pager/render.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stripAnsi = void 0;
|
|
4
4
|
exports.renderPager = renderPager;
|
|
5
|
-
const
|
|
5
|
+
const logo_1 = require("../logo");
|
|
6
6
|
const model_1 = require("./model");
|
|
7
7
|
const theme_1 = require("./theme");
|
|
8
8
|
Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
|
|
@@ -101,28 +101,22 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
|
|
|
101
101
|
function isIdleWelcome(state) {
|
|
102
102
|
return !state.conversationId && state.entries.length === 0;
|
|
103
103
|
}
|
|
104
|
-
function center(text, width, vis = (0,
|
|
104
|
+
function center(text, width, vis = (0, logo_1.displayWidth)(text)) {
|
|
105
105
|
const padLeft = Math.max(0, Math.floor((width - vis) / 2));
|
|
106
106
|
return `${' '.repeat(padLeft)}${text}`;
|
|
107
107
|
}
|
|
108
|
-
function welcomeLines(guest, width, height,
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
return [
|
|
122
|
-
...mark,
|
|
123
|
-
'',
|
|
124
|
-
theme.dim(center('Ask anything, or type /help.', width)),
|
|
125
|
-
];
|
|
108
|
+
function welcomeLines(guest, width, height, _theme) {
|
|
109
|
+
const copy = (0, logo_1.sisuWelcomeCopy)(guest, true).map((line) => center(line, width));
|
|
110
|
+
const treeCols = height >= 18 && width >= 48 ? Math.min(width, 72) : width >= 36 ? 44 : 28;
|
|
111
|
+
const raw = (0, logo_1.sisuTreeLines)(treeCols);
|
|
112
|
+
const painted = (0, logo_1.sisuTreeArt)(treeCols, true).split('\n');
|
|
113
|
+
const treeWidth = raw.reduce((max, line) => Math.max(max, line.length), 0);
|
|
114
|
+
const treePad = Math.max(0, Math.floor((width - treeWidth) / 2));
|
|
115
|
+
const tree = painted.map((line, i) => {
|
|
116
|
+
const vis = raw[i]?.length ?? (0, logo_1.displayWidth)(line);
|
|
117
|
+
return `${' '.repeat(treePad)}${line}${' '.repeat(Math.max(0, treeWidth - vis))}`;
|
|
118
|
+
});
|
|
119
|
+
return [...copy, '', ...tree];
|
|
126
120
|
}
|
|
127
121
|
function slashMenuLines(state, theme) {
|
|
128
122
|
if (!state.slashOpen)
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Volumetric 溯源之树: 3D L-system, z-buffer, lighting, grow + orbit. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.buildTree3d = buildTree3d;
|
|
5
|
+
exports.treeFrameHeight = treeFrameHeight;
|
|
6
|
+
exports.treeFrameWidth = treeFrameWidth;
|
|
7
|
+
exports.renderTreeFrame = renderTreeFrame;
|
|
8
|
+
exports.sisuTreeLines = sisuTreeLines;
|
|
9
|
+
exports.sisuTreeArt = sisuTreeArt;
|
|
10
|
+
exports.sisuTreeWidth = sisuTreeWidth;
|
|
11
|
+
exports.sisuTreeHeight = sisuTreeHeight;
|
|
12
|
+
const RESET = '\x1b[0m';
|
|
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
|
+
}
|
|
29
|
+
function lerp(a, b, t) {
|
|
30
|
+
return a + (b - a) * t;
|
|
31
|
+
}
|
|
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];
|
|
35
|
+
}
|
|
36
|
+
function cross(a, b) {
|
|
37
|
+
return [
|
|
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],
|
|
41
|
+
];
|
|
42
|
+
}
|
|
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);
|
|
111
|
+
}
|
|
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');
|
|
210
|
+
}
|
|
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))
|
|
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');
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
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');
|
|
306
|
+
}
|
|
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');
|
|
316
|
+
}
|
|
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
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function sisuTreeWidth(columns = 80) {
|
|
327
|
+
return treeFrameWidth(columns);
|
|
328
|
+
}
|
|
329
|
+
function sisuTreeHeight(columns = 80) {
|
|
330
|
+
return treeFrameHeight(columns);
|
|
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 (
|
|
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) => {
|