@pep/term-deck 1.0.10
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/LICENSE +21 -0
- package/README.md +356 -0
- package/bin/term-deck.ts +45 -0
- package/examples/slides/01-welcome.md +9 -0
- package/examples/slides/02-features.md +12 -0
- package/examples/slides/03-colors.md +17 -0
- package/examples/slides/04-ascii-art.md +11 -0
- package/examples/slides/05-gradients.md +14 -0
- package/examples/slides/06-themes.md +13 -0
- package/examples/slides/07-markdown.md +13 -0
- package/examples/slides/08-controls.md +13 -0
- package/examples/slides/09-thanks.md +11 -0
- package/examples/slides/deck.config.ts +13 -0
- package/examples/slides-hacker/01-welcome.md +9 -0
- package/examples/slides-hacker/02-features.md +12 -0
- package/examples/slides-hacker/03-colors.md +17 -0
- package/examples/slides-hacker/04-ascii-art.md +11 -0
- package/examples/slides-hacker/05-gradients.md +14 -0
- package/examples/slides-hacker/06-themes.md +13 -0
- package/examples/slides-hacker/07-markdown.md +13 -0
- package/examples/slides-hacker/08-controls.md +13 -0
- package/examples/slides-hacker/09-thanks.md +11 -0
- package/examples/slides-hacker/deck.config.ts +13 -0
- package/examples/slides-matrix/01-welcome.md +9 -0
- package/examples/slides-matrix/02-features.md +12 -0
- package/examples/slides-matrix/03-colors.md +17 -0
- package/examples/slides-matrix/04-ascii-art.md +11 -0
- package/examples/slides-matrix/05-gradients.md +14 -0
- package/examples/slides-matrix/06-themes.md +13 -0
- package/examples/slides-matrix/07-markdown.md +13 -0
- package/examples/slides-matrix/08-controls.md +13 -0
- package/examples/slides-matrix/09-thanks.md +11 -0
- package/examples/slides-matrix/deck.config.ts +13 -0
- package/examples/slides-minimal/01-welcome.md +9 -0
- package/examples/slides-minimal/02-features.md +12 -0
- package/examples/slides-minimal/03-colors.md +17 -0
- package/examples/slides-minimal/04-ascii-art.md +11 -0
- package/examples/slides-minimal/05-gradients.md +14 -0
- package/examples/slides-minimal/06-themes.md +13 -0
- package/examples/slides-minimal/07-markdown.md +13 -0
- package/examples/slides-minimal/08-controls.md +13 -0
- package/examples/slides-minimal/09-thanks.md +11 -0
- package/examples/slides-minimal/deck.config.ts +13 -0
- package/examples/slides-neon/01-welcome.md +9 -0
- package/examples/slides-neon/02-features.md +12 -0
- package/examples/slides-neon/03-colors.md +17 -0
- package/examples/slides-neon/04-ascii-art.md +11 -0
- package/examples/slides-neon/05-gradients.md +14 -0
- package/examples/slides-neon/06-themes.md +13 -0
- package/examples/slides-neon/07-markdown.md +13 -0
- package/examples/slides-neon/08-controls.md +13 -0
- package/examples/slides-neon/09-thanks.md +11 -0
- package/examples/slides-neon/deck.config.ts +13 -0
- package/examples/slides-retro/01-welcome.md +9 -0
- package/examples/slides-retro/02-features.md +12 -0
- package/examples/slides-retro/03-colors.md +17 -0
- package/examples/slides-retro/04-ascii-art.md +11 -0
- package/examples/slides-retro/05-gradients.md +14 -0
- package/examples/slides-retro/06-themes.md +13 -0
- package/examples/slides-retro/07-markdown.md +13 -0
- package/examples/slides-retro/08-controls.md +13 -0
- package/examples/slides-retro/09-thanks.md +11 -0
- package/examples/slides-retro/deck.config.ts +13 -0
- package/package.json +66 -0
- package/src/cli/__tests__/errors.test.ts +201 -0
- package/src/cli/__tests__/help.test.ts +157 -0
- package/src/cli/__tests__/init.test.ts +110 -0
- package/src/cli/commands/export.ts +33 -0
- package/src/cli/commands/init.ts +125 -0
- package/src/cli/commands/present.ts +29 -0
- package/src/cli/errors.ts +77 -0
- package/src/core/__tests__/slide.test.ts +1759 -0
- package/src/core/__tests__/theme.test.ts +1103 -0
- package/src/core/slide.ts +509 -0
- package/src/core/theme.ts +388 -0
- package/src/export/__tests__/recorder.test.ts +566 -0
- package/src/export/recorder.ts +639 -0
- package/src/index.ts +36 -0
- package/src/presenter/__tests__/main.test.ts +244 -0
- package/src/presenter/main.ts +658 -0
- package/src/renderer/__tests__/screen-extended.test.ts +801 -0
- package/src/renderer/__tests__/screen.test.ts +525 -0
- package/src/renderer/screen.ts +671 -0
- package/src/schemas/__tests__/config.test.ts +429 -0
- package/src/schemas/__tests__/slide.test.ts +349 -0
- package/src/schemas/__tests__/theme.test.ts +970 -0
- package/src/schemas/__tests__/validation.test.ts +256 -0
- package/src/schemas/config.ts +58 -0
- package/src/schemas/slide.ts +56 -0
- package/src/schemas/theme.ts +203 -0
- package/src/schemas/validation.ts +64 -0
- package/src/themes/matrix/index.ts +53 -0
- package/themes/hacker.ts +53 -0
- package/themes/minimal.ts +53 -0
- package/themes/neon.ts +53 -0
- package/themes/retro.ts +53 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import blessed from 'neo-blessed'
|
|
2
|
+
import figlet from 'figlet'
|
|
3
|
+
import gradient from 'gradient-string'
|
|
4
|
+
import type { Theme } from '../schemas/theme.js'
|
|
5
|
+
import type { Slide } from '../schemas/slide.js'
|
|
6
|
+
import { normalizeBigText, processSlideContent } from '../core/slide.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main renderer state.
|
|
10
|
+
* Manages the blessed screen, matrix rain background, window stack, and theme.
|
|
11
|
+
*/
|
|
12
|
+
export interface Renderer {
|
|
13
|
+
/** The blessed screen instance */
|
|
14
|
+
screen: blessed.Widgets.Screen
|
|
15
|
+
/** Box element for matrix rain background */
|
|
16
|
+
matrixBox: blessed.Widgets.BoxElement
|
|
17
|
+
/** Stack of window elements (slides render on top of each other) */
|
|
18
|
+
windowStack: blessed.Widgets.BoxElement[]
|
|
19
|
+
/** Active theme for rendering */
|
|
20
|
+
theme: Theme
|
|
21
|
+
/** Array of matrix rain drops for animation */
|
|
22
|
+
matrixDrops: MatrixDrop[]
|
|
23
|
+
/** Interval timer for matrix rain animation (null if stopped) */
|
|
24
|
+
matrixInterval: NodeJS.Timer | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Matrix rain drop.
|
|
29
|
+
* Represents a single falling column of glyphs in the matrix background.
|
|
30
|
+
*/
|
|
31
|
+
export interface MatrixDrop {
|
|
32
|
+
/** Horizontal position (column) */
|
|
33
|
+
x: number
|
|
34
|
+
/** Vertical position (row, can be fractional for smooth animation) */
|
|
35
|
+
y: number
|
|
36
|
+
/** Fall speed (rows per animation frame) */
|
|
37
|
+
speed: number
|
|
38
|
+
/** Array of glyph characters forming the drop's trail */
|
|
39
|
+
trail: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Window creation options.
|
|
44
|
+
* Configuration for creating slide windows with stacking effect.
|
|
45
|
+
*/
|
|
46
|
+
export interface WindowOptions {
|
|
47
|
+
/** Window title displayed in the border */
|
|
48
|
+
title: string
|
|
49
|
+
/** Border color (defaults to theme-based cycling) */
|
|
50
|
+
color?: string
|
|
51
|
+
/** Window width (number for absolute, string for percentage) */
|
|
52
|
+
width?: number | string
|
|
53
|
+
/** Window height (number for absolute, string for percentage) */
|
|
54
|
+
height?: number | string
|
|
55
|
+
/** Top position (number for absolute, string for percentage) */
|
|
56
|
+
top?: number | string
|
|
57
|
+
/** Left position (number for absolute, string for percentage) */
|
|
58
|
+
left?: number | string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Rendered slide content.
|
|
63
|
+
* Structured content ready for display in a window.
|
|
64
|
+
*/
|
|
65
|
+
export interface RenderedContent {
|
|
66
|
+
/** ASCII art big text (from figlet) */
|
|
67
|
+
bigText?: string
|
|
68
|
+
/** Main body content */
|
|
69
|
+
body: string
|
|
70
|
+
/** Mermaid diagram converted to ASCII */
|
|
71
|
+
diagram?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create the main blessed screen.
|
|
76
|
+
* Configures the screen with optimal settings for presentation:
|
|
77
|
+
* - smartCSR: Enables smart cursor movement for efficient rendering
|
|
78
|
+
* - fullUnicode: Enables full unicode support for glyphs
|
|
79
|
+
* - altScreen: Uses alternate screen buffer (preserves terminal on exit)
|
|
80
|
+
* - mouse: Disabled (keyboard-only navigation)
|
|
81
|
+
*
|
|
82
|
+
* @param title - Window title (defaults to 'term-deck')
|
|
83
|
+
* @returns Configured blessed screen instance
|
|
84
|
+
*/
|
|
85
|
+
export function createScreen(title: string = 'term-deck'): blessed.Widgets.Screen {
|
|
86
|
+
const screen = blessed.screen({
|
|
87
|
+
smartCSR: true,
|
|
88
|
+
title,
|
|
89
|
+
fullUnicode: true,
|
|
90
|
+
mouse: false,
|
|
91
|
+
altScreen: true,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return screen
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a trail of random glyphs.
|
|
99
|
+
* Randomly selects glyphs from the theme's glyph set to form a drop trail.
|
|
100
|
+
*
|
|
101
|
+
* @param glyphs - String of available glyphs to choose from
|
|
102
|
+
* @param length - Number of characters in the trail
|
|
103
|
+
* @returns Array of random glyph characters
|
|
104
|
+
*/
|
|
105
|
+
function generateTrail(glyphs: string, length: number): string[] {
|
|
106
|
+
return Array.from({ length }, () =>
|
|
107
|
+
glyphs[Math.floor(Math.random() * glyphs.length)]
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Render one frame of matrix rain.
|
|
113
|
+
* Updates the matrix background with falling glyph trails.
|
|
114
|
+
* This function is called repeatedly by the animation interval.
|
|
115
|
+
*
|
|
116
|
+
* @param renderer - The renderer instance to update
|
|
117
|
+
*/
|
|
118
|
+
export function renderMatrixRain(renderer: Renderer): void {
|
|
119
|
+
const { screen, matrixBox, matrixDrops, theme } = renderer
|
|
120
|
+
const width = Math.max(20, (screen.width as number) || 80)
|
|
121
|
+
const height = Math.max(10, (screen.height as number) || 24)
|
|
122
|
+
|
|
123
|
+
// Create grid for positioning characters
|
|
124
|
+
const grid: string[][] = Array.from({ length: height }, () =>
|
|
125
|
+
Array(width).fill(' ')
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// Update and render drops
|
|
129
|
+
for (const drop of matrixDrops) {
|
|
130
|
+
drop.y += drop.speed
|
|
131
|
+
|
|
132
|
+
// Reset if off screen
|
|
133
|
+
if (drop.y > height + drop.trail.length) {
|
|
134
|
+
drop.y = -drop.trail.length
|
|
135
|
+
drop.x = Math.floor(Math.random() * width)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Draw trail
|
|
139
|
+
for (let i = 0; i < drop.trail.length; i++) {
|
|
140
|
+
const y = Math.floor(drop.y) - i
|
|
141
|
+
if (y >= 0 && y < height && drop.x < width) {
|
|
142
|
+
grid[y][drop.x] = drop.trail[i]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Convert grid to string with colors
|
|
148
|
+
let output = ''
|
|
149
|
+
for (let y = 0; y < height; y++) {
|
|
150
|
+
for (let x = 0; x < width; x++) {
|
|
151
|
+
const char = grid[y][x]
|
|
152
|
+
if (char !== ' ') {
|
|
153
|
+
const brightness = Math.random() > 0.7 ? '{bold}' : ''
|
|
154
|
+
output += `${brightness}{${theme.colors.primary}-fg}${char}{/}`
|
|
155
|
+
} else {
|
|
156
|
+
output += ' '
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (y < height - 1) output += '\n'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
matrixBox.setContent(output)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Initialize matrix rain drops.
|
|
167
|
+
* Creates the initial set of drops and starts the animation loop.
|
|
168
|
+
* This is called automatically by createRenderer.
|
|
169
|
+
*
|
|
170
|
+
* @param renderer - The renderer instance to initialize
|
|
171
|
+
*/
|
|
172
|
+
export function initMatrixRain(renderer: Renderer): void {
|
|
173
|
+
const { screen, theme } = renderer
|
|
174
|
+
const width = (screen.width as number) || 80
|
|
175
|
+
const height = (screen.height as number) || 24
|
|
176
|
+
const density = theme.animations.matrixDensity
|
|
177
|
+
|
|
178
|
+
renderer.matrixDrops = []
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < density; i++) {
|
|
181
|
+
renderer.matrixDrops.push({
|
|
182
|
+
x: Math.floor(Math.random() * width),
|
|
183
|
+
y: Math.floor(Math.random() * height),
|
|
184
|
+
speed: 0.3 + Math.random() * 0.7,
|
|
185
|
+
trail: generateTrail(theme.glyphs, 5 + Math.floor(Math.random() * 10)),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Start animation loop
|
|
190
|
+
renderer.matrixInterval = setInterval(() => {
|
|
191
|
+
renderMatrixRain(renderer)
|
|
192
|
+
renderer.screen.render()
|
|
193
|
+
}, theme.animations.matrixInterval)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create the renderer with all components.
|
|
198
|
+
* Initializes the blessed screen, matrix rain background box,
|
|
199
|
+
* empty window stack, and starts the matrix rain animation.
|
|
200
|
+
*
|
|
201
|
+
* @param theme - Theme configuration for rendering
|
|
202
|
+
* @returns Fully initialized Renderer instance
|
|
203
|
+
*/
|
|
204
|
+
export function createRenderer(theme: Theme): Renderer {
|
|
205
|
+
const screen = createScreen()
|
|
206
|
+
|
|
207
|
+
// Create matrix background box covering full screen
|
|
208
|
+
const matrixBox = blessed.box({
|
|
209
|
+
top: 0,
|
|
210
|
+
left: 0,
|
|
211
|
+
width: '100%',
|
|
212
|
+
height: '100%',
|
|
213
|
+
tags: true,
|
|
214
|
+
})
|
|
215
|
+
screen.append(matrixBox)
|
|
216
|
+
|
|
217
|
+
const renderer: Renderer = {
|
|
218
|
+
screen,
|
|
219
|
+
matrixBox,
|
|
220
|
+
windowStack: [],
|
|
221
|
+
theme,
|
|
222
|
+
matrixDrops: [],
|
|
223
|
+
matrixInterval: null,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Initialize matrix rain
|
|
227
|
+
initMatrixRain(renderer)
|
|
228
|
+
|
|
229
|
+
return renderer
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Destroy renderer and cleanup resources.
|
|
234
|
+
* Stops the matrix rain animation, destroys all windows in the stack,
|
|
235
|
+
* and destroys the blessed screen to restore the terminal.
|
|
236
|
+
*
|
|
237
|
+
* @param renderer - The renderer instance to destroy
|
|
238
|
+
*/
|
|
239
|
+
export function destroyRenderer(renderer: Renderer): void {
|
|
240
|
+
// Clear matrix rain animation interval
|
|
241
|
+
if (renderer.matrixInterval) {
|
|
242
|
+
clearInterval(renderer.matrixInterval)
|
|
243
|
+
renderer.matrixInterval = null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Destroy all windows in the stack
|
|
247
|
+
for (const win of renderer.windowStack) {
|
|
248
|
+
win.destroy()
|
|
249
|
+
}
|
|
250
|
+
renderer.windowStack = []
|
|
251
|
+
|
|
252
|
+
// Destroy the screen (restores terminal)
|
|
253
|
+
renderer.screen.destroy()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get window border color based on index.
|
|
258
|
+
* Cycles through theme colors and additional cyberpunk colors
|
|
259
|
+
* to create a stacking effect with varied border colors.
|
|
260
|
+
*
|
|
261
|
+
* @param index - Window index in the stack
|
|
262
|
+
* @param theme - Theme configuration
|
|
263
|
+
* @returns Hex color string for the window border
|
|
264
|
+
*/
|
|
265
|
+
export function getWindowColor(index: number, theme: Theme): string {
|
|
266
|
+
const colors = [
|
|
267
|
+
theme.colors.primary,
|
|
268
|
+
theme.colors.accent,
|
|
269
|
+
theme.colors.secondary ?? theme.colors.primary,
|
|
270
|
+
'#ff0066', // pink
|
|
271
|
+
'#9966ff', // purple
|
|
272
|
+
'#ffcc00', // yellow
|
|
273
|
+
]
|
|
274
|
+
return colors[index % colors.length]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create a slide window with stacking effect.
|
|
279
|
+
* Creates a bordered box element with theme-based styling,
|
|
280
|
+
* random position for stacking effect, and adds it to the window stack.
|
|
281
|
+
*
|
|
282
|
+
* @param renderer - The renderer instance
|
|
283
|
+
* @param options - Window configuration options
|
|
284
|
+
* @returns The created window box element
|
|
285
|
+
*/
|
|
286
|
+
export function createWindow(
|
|
287
|
+
renderer: Renderer,
|
|
288
|
+
options: WindowOptions
|
|
289
|
+
): blessed.Widgets.BoxElement {
|
|
290
|
+
const { screen, windowStack, theme } = renderer
|
|
291
|
+
const windowIndex = windowStack.length
|
|
292
|
+
const color = options.color ?? getWindowColor(windowIndex, theme)
|
|
293
|
+
|
|
294
|
+
const screenWidth = (screen.width as number) || 120
|
|
295
|
+
const screenHeight = (screen.height as number) || 40
|
|
296
|
+
|
|
297
|
+
// Default dimensions: 75% width, 70% height
|
|
298
|
+
const width = options.width ?? Math.floor(screenWidth * 0.75)
|
|
299
|
+
const height = options.height ?? Math.floor(screenHeight * 0.7)
|
|
300
|
+
|
|
301
|
+
// Random position within bounds (for stacking effect)
|
|
302
|
+
const maxTop = Math.max(1, screenHeight - (height as number) - 2)
|
|
303
|
+
const maxLeft = Math.max(1, screenWidth - (width as number) - 2)
|
|
304
|
+
const top = options.top ?? Math.floor(Math.random() * maxTop)
|
|
305
|
+
const left = options.left ?? Math.floor(Math.random() * maxLeft)
|
|
306
|
+
|
|
307
|
+
const window = theme.window ?? { borderStyle: 'line', shadow: true }
|
|
308
|
+
const padding = window.padding ?? { top: 1, bottom: 1, left: 2, right: 2 }
|
|
309
|
+
|
|
310
|
+
const box = blessed.box({
|
|
311
|
+
top,
|
|
312
|
+
left,
|
|
313
|
+
width,
|
|
314
|
+
height,
|
|
315
|
+
border: {
|
|
316
|
+
type: window.borderStyle === 'none' ? undefined : 'line',
|
|
317
|
+
},
|
|
318
|
+
label: ` ${options.title} `,
|
|
319
|
+
style: {
|
|
320
|
+
fg: theme.colors.text,
|
|
321
|
+
bg: theme.colors.background,
|
|
322
|
+
border: { fg: color },
|
|
323
|
+
label: { fg: color, bold: true },
|
|
324
|
+
},
|
|
325
|
+
padding,
|
|
326
|
+
tags: true,
|
|
327
|
+
shadow: window.shadow,
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
screen.append(box)
|
|
331
|
+
windowStack.push(box)
|
|
332
|
+
|
|
333
|
+
return box
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clear all windows from stack.
|
|
338
|
+
* Destroys all window elements in the stack and resets the stack to empty.
|
|
339
|
+
* This is typically called when transitioning between slides.
|
|
340
|
+
*
|
|
341
|
+
* @param renderer - The renderer instance containing the window stack
|
|
342
|
+
*/
|
|
343
|
+
export function clearWindows(renderer: Renderer): void {
|
|
344
|
+
for (const window of renderer.windowStack) {
|
|
345
|
+
window.destroy()
|
|
346
|
+
}
|
|
347
|
+
renderer.windowStack = []
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Generate ASCII art text with gradient.
|
|
352
|
+
* Uses figlet to convert text to ASCII art and applies a gradient color effect.
|
|
353
|
+
* This function is asynchronous because figlet uses a callback-based API.
|
|
354
|
+
*
|
|
355
|
+
* @param text - The text to convert to ASCII art
|
|
356
|
+
* @param gradientColors - Array of hex colors for the gradient effect
|
|
357
|
+
* @param font - Figlet font to use (defaults to 'Standard')
|
|
358
|
+
* @returns Promise resolving to the gradient-colored ASCII art text
|
|
359
|
+
*/
|
|
360
|
+
export async function generateBigText(
|
|
361
|
+
text: string,
|
|
362
|
+
gradientColors: string[],
|
|
363
|
+
font: string = 'Standard'
|
|
364
|
+
): Promise<string> {
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
figlet.text(text, { font }, (err, result) => {
|
|
367
|
+
if (err || !result) {
|
|
368
|
+
reject(err ?? new Error('Failed to generate figlet text'))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Apply gradient
|
|
373
|
+
const gradientFn = gradient(gradientColors)
|
|
374
|
+
resolve(gradientFn(result))
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generate multi-line big text (for arrays like ['SPEC', 'MACHINE']).
|
|
381
|
+
* Creates ASCII art for each line separately and joins them with newlines.
|
|
382
|
+
* Each line gets the same gradient applied independently.
|
|
383
|
+
*
|
|
384
|
+
* @param lines - Array of text strings to convert to ASCII art
|
|
385
|
+
* @param gradientColors - Array of hex colors for the gradient effect
|
|
386
|
+
* @param font - Figlet font to use (defaults to 'Standard')
|
|
387
|
+
* @returns Promise resolving to the combined gradient-colored ASCII art
|
|
388
|
+
*/
|
|
389
|
+
export async function generateMultiLineBigText(
|
|
390
|
+
lines: string[],
|
|
391
|
+
gradientColors: string[],
|
|
392
|
+
font: string = 'Standard'
|
|
393
|
+
): Promise<string> {
|
|
394
|
+
const results = await Promise.all(
|
|
395
|
+
lines.map((line) => generateBigText(line, gradientColors, font))
|
|
396
|
+
)
|
|
397
|
+
return results.join('\n')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Characters used for glitch effect (avoiding box drawing).
|
|
402
|
+
* These characters create a cyberpunk glitch aesthetic when scrambling text.
|
|
403
|
+
* Includes: block characters, shapes, math symbols, Greek letters, and katakana.
|
|
404
|
+
*/
|
|
405
|
+
const GLITCH_CHARS =
|
|
406
|
+
'█▓▒░▀▄▌▐■□▪▫●○◊◘◙♦♣♠♥★☆⌂ⁿ²³ÆØ∞≈≠±×÷αβγδεζηθλμπσφωΔΣΩアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン'
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Characters to never glitch.
|
|
410
|
+
* Protects structural characters like spaces, punctuation, box drawing,
|
|
411
|
+
* and arrows to maintain readability and layout integrity during glitch effects.
|
|
412
|
+
*/
|
|
413
|
+
const PROTECTED_CHARS = new Set([
|
|
414
|
+
' ', '\t', '\n', '{', '}', '-', '/', '#', '[', ']', '(', ')', ':', ';',
|
|
415
|
+
',', '.', '!', '?', "'", '"', '`', '_', '|', '\\', '<', '>', '=', '+',
|
|
416
|
+
'*', '&', '^', '%', '$', '@', '~',
|
|
417
|
+
// Box drawing
|
|
418
|
+
'┌', '┐', '└', '┘', '│', '─', '├', '┤', '┬', '┴', '┼', '═', '║',
|
|
419
|
+
'╔', '╗', '╚', '╝', '╠', '╣', '╦', '╩', '╬', '╭', '╮', '╯', '╰',
|
|
420
|
+
// Arrows
|
|
421
|
+
'→', '←', '↑', '↓', '▶', '◀', '▲', '▼', '►', '◄',
|
|
422
|
+
])
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Sleep helper for async animations.
|
|
426
|
+
* Returns a promise that resolves after the specified delay.
|
|
427
|
+
*
|
|
428
|
+
* @param ms - Delay in milliseconds
|
|
429
|
+
* @returns Promise that resolves after the delay
|
|
430
|
+
*/
|
|
431
|
+
function sleep(ms: number): Promise<void> {
|
|
432
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Glitch-reveal a single line of text.
|
|
437
|
+
* Animates the transition from scrambled characters to the final text.
|
|
438
|
+
* The scramble ratio decreases with each iteration, gradually revealing the text.
|
|
439
|
+
* Protected characters (spaces, punctuation, box drawing) are never scrambled.
|
|
440
|
+
*
|
|
441
|
+
* @param box - The blessed box element to render into
|
|
442
|
+
* @param screen - The blessed screen for rendering
|
|
443
|
+
* @param currentLines - Array of already-revealed lines
|
|
444
|
+
* @param newLine - The new line to glitch-reveal
|
|
445
|
+
* @param iterations - Number of glitch iterations (default: 5)
|
|
446
|
+
*/
|
|
447
|
+
export async function glitchLine(
|
|
448
|
+
box: blessed.Widgets.BoxElement,
|
|
449
|
+
screen: blessed.Widgets.Screen,
|
|
450
|
+
currentLines: string[],
|
|
451
|
+
newLine: string,
|
|
452
|
+
iterations: number = 5
|
|
453
|
+
): Promise<void> {
|
|
454
|
+
for (let i = iterations; i >= 0; i--) {
|
|
455
|
+
const scrambleRatio = i / iterations
|
|
456
|
+
let scrambledLine = ''
|
|
457
|
+
|
|
458
|
+
for (const char of newLine) {
|
|
459
|
+
if (PROTECTED_CHARS.has(char)) {
|
|
460
|
+
scrambledLine += char
|
|
461
|
+
} else if (Math.random() < scrambleRatio) {
|
|
462
|
+
scrambledLine += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)]
|
|
463
|
+
} else {
|
|
464
|
+
scrambledLine += char
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
box.setContent([...currentLines, scrambledLine].join('\n'))
|
|
469
|
+
screen.render()
|
|
470
|
+
await sleep(20)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Reveal content line by line with glitch effect.
|
|
476
|
+
* Animates the transition of multi-line content by revealing each line
|
|
477
|
+
* sequentially with a glitch effect. Uses theme-configured line delay
|
|
478
|
+
* and glitch iteration count for consistent animation timing.
|
|
479
|
+
*
|
|
480
|
+
* @param box - The blessed box element to render into
|
|
481
|
+
* @param screen - The blessed screen for rendering
|
|
482
|
+
* @param content - The complete content string (with newlines)
|
|
483
|
+
* @param theme - Theme configuration for animation timing
|
|
484
|
+
*/
|
|
485
|
+
export async function lineByLineReveal(
|
|
486
|
+
box: blessed.Widgets.BoxElement,
|
|
487
|
+
screen: blessed.Widgets.Screen,
|
|
488
|
+
content: string,
|
|
489
|
+
theme: Theme
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
const lines = content.split('\n')
|
|
492
|
+
const revealedLines: string[] = []
|
|
493
|
+
const lineDelay = theme.animations.lineDelay
|
|
494
|
+
const glitchIterations = theme.animations.glitchIterations
|
|
495
|
+
|
|
496
|
+
for (const line of lines) {
|
|
497
|
+
await glitchLine(box, screen, revealedLines, line, glitchIterations)
|
|
498
|
+
revealedLines.push(line)
|
|
499
|
+
box.setContent(revealedLines.join('\n'))
|
|
500
|
+
screen.render()
|
|
501
|
+
|
|
502
|
+
// Delay between lines (skip for empty lines)
|
|
503
|
+
if (line.trim()) {
|
|
504
|
+
await sleep(lineDelay)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Transition type for slide animations.
|
|
511
|
+
* Defines the available transition effects for revealing slide content.
|
|
512
|
+
*/
|
|
513
|
+
export type TransitionType = 'glitch' | 'fade' | 'instant' | 'typewriter'
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Fade-in reveal (character by character, all at once).
|
|
517
|
+
* Gradually reveals characters randomly across the entire content
|
|
518
|
+
* to create a fade-in effect. Uses multiple steps with increasing
|
|
519
|
+
* reveal probability for a smooth animation.
|
|
520
|
+
*
|
|
521
|
+
* @param box - The blessed box element to render into
|
|
522
|
+
* @param screen - The blessed screen for rendering
|
|
523
|
+
* @param content - The complete content string to reveal
|
|
524
|
+
* @param theme - Theme configuration for animation timing
|
|
525
|
+
*/
|
|
526
|
+
async function fadeInReveal(
|
|
527
|
+
box: blessed.Widgets.BoxElement,
|
|
528
|
+
screen: blessed.Widgets.Screen,
|
|
529
|
+
content: string,
|
|
530
|
+
theme: Theme
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
const steps = 10
|
|
533
|
+
const delay = (theme.animations.lineDelay * 2) / steps
|
|
534
|
+
|
|
535
|
+
for (let step = 0; step < steps; step++) {
|
|
536
|
+
const revealRatio = step / steps
|
|
537
|
+
let revealed = ''
|
|
538
|
+
|
|
539
|
+
for (const char of content) {
|
|
540
|
+
if (char === '\n' || PROTECTED_CHARS.has(char) || Math.random() < revealRatio) {
|
|
541
|
+
revealed += char
|
|
542
|
+
} else {
|
|
543
|
+
revealed += ' '
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
box.setContent(revealed)
|
|
548
|
+
screen.render()
|
|
549
|
+
await sleep(delay)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
box.setContent(content)
|
|
553
|
+
screen.render()
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Typewriter reveal (character by character, sequentially).
|
|
558
|
+
* Reveals content character by character in order, like a typewriter.
|
|
559
|
+
* Skips delay for spaces and newlines to maintain smooth flow.
|
|
560
|
+
*
|
|
561
|
+
* @param box - The blessed box element to render into
|
|
562
|
+
* @param screen - The blessed screen for rendering
|
|
563
|
+
* @param content - The complete content string to reveal
|
|
564
|
+
* @param theme - Theme configuration for animation timing
|
|
565
|
+
*/
|
|
566
|
+
async function typewriterReveal(
|
|
567
|
+
box: blessed.Widgets.BoxElement,
|
|
568
|
+
screen: blessed.Widgets.Screen,
|
|
569
|
+
content: string,
|
|
570
|
+
theme: Theme
|
|
571
|
+
): Promise<void> {
|
|
572
|
+
const charDelay = theme.animations.lineDelay / 5
|
|
573
|
+
let revealed = ''
|
|
574
|
+
|
|
575
|
+
for (const char of content) {
|
|
576
|
+
revealed += char
|
|
577
|
+
box.setContent(revealed)
|
|
578
|
+
screen.render()
|
|
579
|
+
|
|
580
|
+
if (char !== ' ' && char !== '\n') {
|
|
581
|
+
await sleep(charDelay)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Apply transition effect to reveal content.
|
|
588
|
+
* Dispatcher function that selects and applies the appropriate
|
|
589
|
+
* transition animation based on the specified transition type.
|
|
590
|
+
*
|
|
591
|
+
* @param box - The blessed box element to render into
|
|
592
|
+
* @param screen - The blessed screen for rendering
|
|
593
|
+
* @param content - The complete content string to reveal
|
|
594
|
+
* @param transition - The type of transition effect to apply
|
|
595
|
+
* @param theme - Theme configuration for animation timing
|
|
596
|
+
*/
|
|
597
|
+
export async function applyTransition(
|
|
598
|
+
box: blessed.Widgets.BoxElement,
|
|
599
|
+
screen: blessed.Widgets.Screen,
|
|
600
|
+
content: string,
|
|
601
|
+
transition: TransitionType,
|
|
602
|
+
theme: Theme
|
|
603
|
+
): Promise<void> {
|
|
604
|
+
switch (transition) {
|
|
605
|
+
case 'glitch':
|
|
606
|
+
await lineByLineReveal(box, screen, content, theme)
|
|
607
|
+
break
|
|
608
|
+
|
|
609
|
+
case 'fade':
|
|
610
|
+
await fadeInReveal(box, screen, content, theme)
|
|
611
|
+
break
|
|
612
|
+
|
|
613
|
+
case 'instant':
|
|
614
|
+
box.setContent(content)
|
|
615
|
+
screen.render()
|
|
616
|
+
break
|
|
617
|
+
|
|
618
|
+
case 'typewriter':
|
|
619
|
+
await typewriterReveal(box, screen, content, theme)
|
|
620
|
+
break
|
|
621
|
+
|
|
622
|
+
default:
|
|
623
|
+
box.setContent(content)
|
|
624
|
+
screen.render()
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Render a slide to a window.
|
|
630
|
+
* Creates a window, generates bigText if present, processes the body content,
|
|
631
|
+
* and applies the specified transition effect to reveal the slide.
|
|
632
|
+
*
|
|
633
|
+
* @param renderer - The renderer instance
|
|
634
|
+
* @param slide - The slide to render
|
|
635
|
+
* @returns The created window box element containing the rendered slide
|
|
636
|
+
*/
|
|
637
|
+
export async function renderSlide(
|
|
638
|
+
renderer: Renderer,
|
|
639
|
+
slide: Slide
|
|
640
|
+
): Promise<blessed.Widgets.BoxElement> {
|
|
641
|
+
const { theme } = renderer
|
|
642
|
+
const { frontmatter, body } = slide
|
|
643
|
+
|
|
644
|
+
// Create window
|
|
645
|
+
const window = createWindow(renderer, {
|
|
646
|
+
title: frontmatter.title,
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Build content
|
|
650
|
+
let content = ''
|
|
651
|
+
|
|
652
|
+
// Big text (figlet)
|
|
653
|
+
const bigTextLines = normalizeBigText(frontmatter.bigText)
|
|
654
|
+
if (bigTextLines.length > 0) {
|
|
655
|
+
const gradientName = frontmatter.gradient ?? 'fire'
|
|
656
|
+
const gradientColors = theme.gradients[gradientName] ?? theme.gradients.fire
|
|
657
|
+
|
|
658
|
+
const bigText = await generateMultiLineBigText(bigTextLines, gradientColors)
|
|
659
|
+
content += bigText + '\n\n'
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Process body content (color tokens, mermaid)
|
|
663
|
+
const processedBody = await processSlideContent(body, theme)
|
|
664
|
+
content += processedBody
|
|
665
|
+
|
|
666
|
+
// Apply transition
|
|
667
|
+
const transition = frontmatter.transition ?? 'glitch'
|
|
668
|
+
await applyTransition(window, renderer.screen, content, transition, theme)
|
|
669
|
+
|
|
670
|
+
return window
|
|
671
|
+
}
|