keyvoid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/bin/keyvoid.js +121 -0
- package/package.json +77 -0
- package/src/app.js +294 -0
- package/src/engine/permissions.js +107 -0
- package/src/engine/suppressor/helpers/linux-helper.py +192 -0
- package/src/engine/suppressor/helpers/macos-helper.swift +108 -0
- package/src/engine/suppressor/helpers/macos-pynput-helper.py +179 -0
- package/src/engine/suppressor/helpers/windows-helper.ps1 +144 -0
- package/src/engine/suppressor/index.js +306 -0
- package/src/scripts/postinstall.js +33 -0
- package/src/ui/components/counter.js +64 -0
- package/src/ui/components/header.js +102 -0
- package/src/ui/components/status-bar.js +71 -0
- package/src/ui/components/unvoid-button.js +78 -0
- package/src/ui/mouse.js +97 -0
- package/src/ui/renderer.js +113 -0
- package/src/ui/skins/arcade.js +530 -0
- package/src/ui/skins/cat.js +223 -0
- package/src/ui/skins/clean.js +155 -0
- package/src/ui/skins/hacker.js +194 -0
- package/src/ui/skins/prank.js +195 -0
- package/src/ui/skins/toddler.js +131 -0
- package/src/ui/skins/zen.js +169 -0
- package/src/ui/unlock-sequence.js +105 -0
- package/src/utils/big-digits.js +130 -0
- package/src/utils/colors.js +114 -0
- package/src/utils/terminal.js +119 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// ─── Prank Skin ──────────────────────────────────────────────────────
|
|
2
|
+
// Fake "Windows Update" screen. Progress bar stuck at 99%.
|
|
3
|
+
// Exit: triple-click in the top-right corner area.
|
|
4
|
+
|
|
5
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
6
|
+
import { palettes, RESET, BOLD, DIM } from '../../utils/colors.js';
|
|
7
|
+
import { centerText, blocks } from '../../utils/terminal.js';
|
|
8
|
+
|
|
9
|
+
const P = palettes.prank;
|
|
10
|
+
|
|
11
|
+
// Fake file operations that scroll
|
|
12
|
+
const FAKE_FILES = [
|
|
13
|
+
'Configuring system32/ntdll.dll...',
|
|
14
|
+
'Updating kernel32.sys...',
|
|
15
|
+
'Installing security patch KB5034441...',
|
|
16
|
+
'Applying cumulative update...',
|
|
17
|
+
'Downloading telemetry modules...',
|
|
18
|
+
'Optimizing system registry...',
|
|
19
|
+
'Reconfiguring boot loader...',
|
|
20
|
+
'Updating display drivers...',
|
|
21
|
+
'Applying .NET Framework 8.5 update...',
|
|
22
|
+
'Installing Windows Defender definitions...',
|
|
23
|
+
'Updating OneDrive integration...',
|
|
24
|
+
'Configuring Hyper-V services...',
|
|
25
|
+
'Applying power management settings...',
|
|
26
|
+
'Updating Bluetooth stack...',
|
|
27
|
+
'Reinstalling Cortana...',
|
|
28
|
+
'Applying Group Policy updates...',
|
|
29
|
+
'Updating Windows Search indexer...',
|
|
30
|
+
'Installing optional features...',
|
|
31
|
+
'Configuring Windows Update service...',
|
|
32
|
+
'Finalizing pending operations...',
|
|
33
|
+
'Running system file checker...',
|
|
34
|
+
'Verifying system integrity...',
|
|
35
|
+
'Applying network stack update...',
|
|
36
|
+
'Updating USB controller firmware...',
|
|
37
|
+
'Installing audio engine update...',
|
|
38
|
+
'Please wait, this will take a while...',
|
|
39
|
+
'Almost done, do not turn off your computer...',
|
|
40
|
+
'Preparing to configure Windows...',
|
|
41
|
+
'Running cleanup tasks...',
|
|
42
|
+
'Registering system components...',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Track secret exit: triple-click in top-right 5×3 area
|
|
46
|
+
let clickTimestamps = [];
|
|
47
|
+
const TRIPLE_CLICK_WINDOW = 1500; // 1.5 seconds
|
|
48
|
+
const TRIPLE_CLICK_ZONE = { x: -10, y: 1, width: 10, height: 3 }; // relative to top-right
|
|
49
|
+
|
|
50
|
+
export function renderPrank(cols, rows, state, tick) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
|
|
53
|
+
// Windows blue background
|
|
54
|
+
const bgLine = `${P.bg}${' '.repeat(cols)}${RESET}`;
|
|
55
|
+
for (let i = 0; i < rows; i++) lines.push(bgLine);
|
|
56
|
+
|
|
57
|
+
// ── Windows-style logo (simplified) ──
|
|
58
|
+
const logoY = Math.floor(rows * 0.15);
|
|
59
|
+
const logo = [
|
|
60
|
+
'⊞',
|
|
61
|
+
];
|
|
62
|
+
lines[logoY] = `${P.bg}${centerText(`${P.primary}${BOLD}⊞`, cols)}${RESET}`;
|
|
63
|
+
|
|
64
|
+
// ── Main message ──
|
|
65
|
+
const msgY = logoY + 2;
|
|
66
|
+
lines[msgY] = `${P.bg}${centerText(
|
|
67
|
+
`${P.primary}${BOLD}Working on updates`,
|
|
68
|
+
cols
|
|
69
|
+
)}${RESET}`;
|
|
70
|
+
|
|
71
|
+
// Animated dots
|
|
72
|
+
const dots = '.'.repeat((Math.floor(tick / 10) % 4));
|
|
73
|
+
lines[msgY + 1] = `${P.bg}${centerText(
|
|
74
|
+
`${P.secondary}${dots.padEnd(3)}`,
|
|
75
|
+
cols
|
|
76
|
+
)}${RESET}`;
|
|
77
|
+
|
|
78
|
+
// ── Progress percentage ──
|
|
79
|
+
// Stuck at 99%, occasionally flickers to 98%
|
|
80
|
+
const pctFlicker = tick % 120 > 110 ? 98 : 99;
|
|
81
|
+
const pctY = msgY + 3;
|
|
82
|
+
lines[pctY] = `${P.bg}${centerText(
|
|
83
|
+
`${P.primary}${BOLD}${pctFlicker}% complete`,
|
|
84
|
+
cols
|
|
85
|
+
)}${RESET}`;
|
|
86
|
+
|
|
87
|
+
// ── Progress bar ──
|
|
88
|
+
const barWidth = Math.min(50, cols - 20);
|
|
89
|
+
const barY = pctY + 2;
|
|
90
|
+
const filledWidth = Math.floor(barWidth * 0.99);
|
|
91
|
+
const emptyWidth = barWidth - filledWidth;
|
|
92
|
+
|
|
93
|
+
// Animated shimmer on the progress bar
|
|
94
|
+
const shimmerPos = Math.floor((tick * 0.5) % (filledWidth + 5));
|
|
95
|
+
|
|
96
|
+
let barStr = '';
|
|
97
|
+
for (let i = 0; i < filledWidth; i++) {
|
|
98
|
+
if (i >= shimmerPos - 2 && i <= shimmerPos + 2) {
|
|
99
|
+
// Bright shimmer
|
|
100
|
+
barStr += `\x1b[48;2;120;180;255m `;
|
|
101
|
+
} else {
|
|
102
|
+
barStr += `\x1b[48;2;255;255;255m `;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
barStr += `\x1b[48;2;0;60;130m${' '.repeat(emptyWidth)}`;
|
|
106
|
+
|
|
107
|
+
// Re-apply the Windows Blue background (P.bg) after barStr, so centerText's right padding uses it
|
|
108
|
+
lines[barY] = `${P.bg}${centerText(`${barStr}${P.bg}`, cols)}${RESET}`;
|
|
109
|
+
|
|
110
|
+
// ── "Don't turn off" message ──
|
|
111
|
+
const warnY = barY + 2;
|
|
112
|
+
lines[warnY] = `${P.bg}${centerText(
|
|
113
|
+
`${P.secondary}Don't turn off your computer`,
|
|
114
|
+
cols
|
|
115
|
+
)}${RESET}`;
|
|
116
|
+
|
|
117
|
+
// ── Scrolling fake file operations ──
|
|
118
|
+
const fileY = warnY + 3;
|
|
119
|
+
const visibleFiles = Math.min(6, rows - fileY - 5);
|
|
120
|
+
const fileOffset = Math.floor(tick / 8) % FAKE_FILES.length;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < visibleFiles; i++) {
|
|
123
|
+
const fileIdx = (fileOffset + i) % FAKE_FILES.length;
|
|
124
|
+
const fade = i === 0 ? P.dim : (i === visibleFiles - 1 ? P.dim : P.secondary);
|
|
125
|
+
const fileRow = fileY + i;
|
|
126
|
+
if (fileRow < rows - 3) {
|
|
127
|
+
lines[fileRow] = `${P.bg}${centerText(
|
|
128
|
+
`${fade}${DIM}${FAKE_FILES[fileIdx]}`,
|
|
129
|
+
cols
|
|
130
|
+
)}${RESET}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── ETA (fake, keeps increasing) ──
|
|
135
|
+
const etaY = fileY + visibleFiles + 1;
|
|
136
|
+
const etaMinutes = 45 + Math.floor(tick / 60);
|
|
137
|
+
if (etaY < rows - 3) {
|
|
138
|
+
lines[etaY] = `${P.bg}${centerText(
|
|
139
|
+
`${P.dim}Estimated time remaining: ${etaMinutes} minutes`,
|
|
140
|
+
cols
|
|
141
|
+
)}${RESET}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Visible hint (bottom of screen) ──
|
|
145
|
+
const hintRow = rows - 1;
|
|
146
|
+
const hintColor = '\x1b[38;2;150;200;255m'; // Light blue, visible but unobtrusive
|
|
147
|
+
const hintText = '(triple click on top right to exit)';
|
|
148
|
+
|
|
149
|
+
// Center the hint text at the bottom
|
|
150
|
+
lines[hintRow] = `${P.bg}${centerText(`${hintColor}${DIM}${hintText}`, cols)}${RESET}`;
|
|
151
|
+
|
|
152
|
+
// NOTE: In prank mode, the UNVOID button is HIDDEN.
|
|
153
|
+
// Exit is via triple-click in the top-right corner area.
|
|
154
|
+
|
|
155
|
+
// The secret clickable region is the top-right 10×3 area
|
|
156
|
+
const secretRegion = {
|
|
157
|
+
x: Math.max(1, cols - 10),
|
|
158
|
+
y: 1,
|
|
159
|
+
width: 10,
|
|
160
|
+
height: 3,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
lines,
|
|
165
|
+
buttonRegion: null, // No visible UNVOID button
|
|
166
|
+
secretRegion, // Secret exit zone
|
|
167
|
+
isPrank: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if a triple-click has occurred in the secret zone
|
|
172
|
+
export function checkPrankExit(clickX, clickY, cols) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
|
|
175
|
+
// Check if click is in the top-right zone
|
|
176
|
+
if (clickX >= cols - 10 && clickY <= 3) {
|
|
177
|
+
clickTimestamps.push(now);
|
|
178
|
+
|
|
179
|
+
// Remove clicks older than the time window
|
|
180
|
+
clickTimestamps = clickTimestamps.filter(t => now - t < TRIPLE_CLICK_WINDOW);
|
|
181
|
+
|
|
182
|
+
// Triple-click detected!
|
|
183
|
+
if (clickTimestamps.length >= 3) {
|
|
184
|
+
clickTimestamps = [];
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Reset prank state
|
|
193
|
+
export function resetPrankState() {
|
|
194
|
+
clickTimestamps = [];
|
|
195
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ─── Toddler Skin ────────────────────────────────────────────────────
|
|
2
|
+
// Fun mode: random emojis, cycling rainbow colors, playful messages.
|
|
3
|
+
// Designed to entertain a child while keeping the system safe.
|
|
4
|
+
|
|
5
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
6
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
7
|
+
import { palettes, rainbow, RESET, BOLD } from '../../utils/colors.js';
|
|
8
|
+
import { centerText } from '../../utils/terminal.js';
|
|
9
|
+
import { formatCount } from '../../utils/big-digits.js';
|
|
10
|
+
|
|
11
|
+
const P = palettes.toddler;
|
|
12
|
+
|
|
13
|
+
// Emoji collections for different "reactions"
|
|
14
|
+
const ANIMALS = ['🦕', '🦖', '🐘', '🦋', '🐙', '🦄', '🐢', '🐬', '🦊', '🐼', '🐨', '🦜', '🐳', '🦁', '🐸'];
|
|
15
|
+
const SPACE = ['🚀', '🌍', '⭐', '🌙', '🛸', '☄️', '🪐', '🌈', '✨', '💫', '🌟', '⚡'];
|
|
16
|
+
const FOOD = ['🍕', '🍦', '🍩', '🧁', '🍪', '🎂', '🍭', '🍬', '🍫', '🍓', '🍉', '🥝'];
|
|
17
|
+
const FUN = ['🎨', '🎪', '🎠', '🎡', '🎵', '🎶', '🎸', '🥁', '🎺', '🎹', '🪩', '🎯'];
|
|
18
|
+
|
|
19
|
+
const ALL_EMOJIS = [...ANIMALS, ...SPACE, ...FOOD, ...FUN];
|
|
20
|
+
|
|
21
|
+
const MESSAGES = [
|
|
22
|
+
'WOW!', 'COOL!', 'AMAZING!', 'YAY!', 'WHEEE!',
|
|
23
|
+
'AWESOME!', 'SUPER!', 'WOOHOO!', 'TADA!', 'MAGIC!',
|
|
24
|
+
'BOOM!', 'POW!', 'ZAP!', 'KABOOM!', 'SPARKLE!',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// State for the toddler animations
|
|
28
|
+
let lastEmoji = '🦕';
|
|
29
|
+
let lastMessage = 'WOW!';
|
|
30
|
+
let currentBgIndex = 0;
|
|
31
|
+
let lastKeyCount = 0;
|
|
32
|
+
|
|
33
|
+
export function renderToddler(cols, rows, state, tick) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
const { keyCount = 0 } = state;
|
|
36
|
+
|
|
37
|
+
// Update emoji and message when new key is pressed
|
|
38
|
+
if (keyCount > lastKeyCount) {
|
|
39
|
+
lastEmoji = ALL_EMOJIS[Math.floor(Math.random() * ALL_EMOJIS.length)];
|
|
40
|
+
lastMessage = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
|
|
41
|
+
currentBgIndex = (currentBgIndex + 1) % rainbow.length;
|
|
42
|
+
lastKeyCount = keyCount;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Cycling background color (shifts even without key presses, slowly)
|
|
46
|
+
const bgIdx = (currentBgIndex + Math.floor(tick / 30)) % rainbow.length;
|
|
47
|
+
const bg = rainbow[bgIdx];
|
|
48
|
+
const bgLine = `${bg}${' '.repeat(cols)}${RESET}`;
|
|
49
|
+
|
|
50
|
+
// Fill background
|
|
51
|
+
for (let i = 0; i < rows; i++) {
|
|
52
|
+
lines.push(bgLine);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Title ──
|
|
56
|
+
const title = '✨ K E Y V O I D ✨';
|
|
57
|
+
const titleColored = `${P.primary}${BOLD}${title}`;
|
|
58
|
+
lines[2] = `${bg}${centerText(titleColored, cols)}${RESET}`;
|
|
59
|
+
|
|
60
|
+
const subtitle = '~ toddler mode ~';
|
|
61
|
+
lines[3] = `${bg}${centerText(`${P.secondary}${subtitle}`, cols)}${RESET}`;
|
|
62
|
+
|
|
63
|
+
// ── Bottom Elements ──
|
|
64
|
+
const statusHeight = getStatusBarHeight(state);
|
|
65
|
+
const buttonRow = Math.max(20, rows - statusHeight - 7);
|
|
66
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
67
|
+
|
|
68
|
+
const countRow = buttonRow - 3;
|
|
69
|
+
const msgRow = countRow - 2;
|
|
70
|
+
|
|
71
|
+
// ── Giant Emoji (Floating in available space) ──
|
|
72
|
+
const availableSpace = msgRow - 5;
|
|
73
|
+
const centerY = 5 + Math.floor(availableSpace / 2);
|
|
74
|
+
const bounce = Math.floor(Math.sin(tick * 0.2) * 2); // Bouncing animation
|
|
75
|
+
const emojiRow = Math.max(5, Math.min(centerY + bounce, rows - 15));
|
|
76
|
+
|
|
77
|
+
// Build a huge emoji display (using text around emoji)
|
|
78
|
+
const sparkles = '✨'.repeat(3);
|
|
79
|
+
const emojiDisplay = `${sparkles} ${lastEmoji} ${sparkles}`;
|
|
80
|
+
lines[emojiRow] = `${bg}${centerText(emojiDisplay, cols)}${RESET}`;
|
|
81
|
+
|
|
82
|
+
// Repeat emoji bigger in surrounding rows
|
|
83
|
+
lines[emojiRow - 1] = `${bg}${centerText(`${lastEmoji} ${lastEmoji} ${lastEmoji}`, cols)}${RESET}`;
|
|
84
|
+
lines[emojiRow + 1] = `${bg}${centerText(`${lastEmoji} ${lastEmoji} ${lastEmoji}`, cols)}${RESET}`;
|
|
85
|
+
|
|
86
|
+
// ── Fun Message ──
|
|
87
|
+
if (msgRow < rows - 5 && msgRow > emojiRow + 1) {
|
|
88
|
+
const msgColors = [P.primary, P.secondary, P.accent, P.fun1, P.fun2, P.fun3, P.fun4];
|
|
89
|
+
const msgColor = msgColors[Math.floor(tick / 5) % msgColors.length];
|
|
90
|
+
const msgStr = `${msgColor}${BOLD} ★ ${lastMessage} ★ `;
|
|
91
|
+
lines[msgRow] = `${bg}${centerText(msgStr, cols)}${RESET}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Key Count (playful) ──
|
|
95
|
+
if (countRow < rows - 5 && countRow > emojiRow + 2) {
|
|
96
|
+
const countStr = `${P.secondary}${formatCount(keyCount)} little taps blocked! 🛡️`;
|
|
97
|
+
lines[countRow] = `${bg}${centerText(countStr, cols)}${RESET}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Stars border animation ──
|
|
101
|
+
const star1 = tick % 4 === 0 ? '⭐' : '✨';
|
|
102
|
+
const star2 = tick % 4 === 2 ? '⭐' : '✨';
|
|
103
|
+
const starLine = (star1 + ' ').repeat(Math.floor(cols / 3));
|
|
104
|
+
lines[0] = `${bg}${starLine.substring(0, cols)}${RESET}`;
|
|
105
|
+
|
|
106
|
+
// ── UNVOID Button ──
|
|
107
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
108
|
+
if (buttonRow + i < rows - statusHeight) {
|
|
109
|
+
lines[buttonRow + i] = `${bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + bg)}${RESET}`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Status Bar ──
|
|
114
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'toddler' }, tick);
|
|
115
|
+
const statusStartRow = rows - statusLines.length;
|
|
116
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
117
|
+
if (statusStartRow + i < rows) {
|
|
118
|
+
lines[statusStartRow + i] = `${bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + bg)}${RESET}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
lines,
|
|
124
|
+
buttonRegion: {
|
|
125
|
+
x: button.x,
|
|
126
|
+
y: buttonRow + 1,
|
|
127
|
+
width: button.width,
|
|
128
|
+
height: button.height,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ─── Zen Skin ─────────────────────────────────────────────────────────
|
|
2
|
+
// A calm, meditative breathing visual. Blocked keys create gentle ripples.
|
|
3
|
+
|
|
4
|
+
import { renderUnvoidButton } from '../components/unvoid-button.js';
|
|
5
|
+
import { renderStatusBar, getStatusBarHeight } from '../components/status-bar.js';
|
|
6
|
+
import { palettes, RESET } from '../../utils/colors.js';
|
|
7
|
+
import { centerText } from '../../utils/terminal.js';
|
|
8
|
+
|
|
9
|
+
const P = palettes.zen;
|
|
10
|
+
let lastKeyCount = 0;
|
|
11
|
+
|
|
12
|
+
// Track active ripples: { radius: 0, life: 1.0 }
|
|
13
|
+
const activeRipples = [];
|
|
14
|
+
|
|
15
|
+
export function renderZen(cols, rows, state, tick) {
|
|
16
|
+
const lines = Array(rows).fill(`${P.bg}${' '.repeat(cols)}${RESET}`);
|
|
17
|
+
const statusHeight = getStatusBarHeight(state);
|
|
18
|
+
const playAreaH = rows - statusHeight - 4;
|
|
19
|
+
|
|
20
|
+
const { keyCount = 0 } = state;
|
|
21
|
+
|
|
22
|
+
// Trigger ripples on key press
|
|
23
|
+
if (keyCount > lastKeyCount) {
|
|
24
|
+
const diff = Math.min(keyCount - lastKeyCount, 3);
|
|
25
|
+
for (let i = 0; i < diff; i++) {
|
|
26
|
+
activeRipples.push({
|
|
27
|
+
radius: 0 - (i * 2), // Stagger slightly if multiple
|
|
28
|
+
life: 1.0
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
lastKeyCount = keyCount;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Update ripples
|
|
35
|
+
for (let i = activeRipples.length - 1; i >= 0; i--) {
|
|
36
|
+
const r = activeRipples[i];
|
|
37
|
+
r.radius += 0.8; // expansion speed
|
|
38
|
+
if (r.radius > 0) {
|
|
39
|
+
r.life -= 0.012; // fade out
|
|
40
|
+
}
|
|
41
|
+
if (r.life <= 0) {
|
|
42
|
+
activeRipples.splice(i, 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Breathing animation (slow sine wave)
|
|
47
|
+
const breath = Math.sin(tick * 0.02); // -1 to 1
|
|
48
|
+
const baseRadius = 8 + breath * 2;
|
|
49
|
+
|
|
50
|
+
const cx = Math.floor(cols / 2);
|
|
51
|
+
const cy = Math.floor(playAreaH / 2);
|
|
52
|
+
|
|
53
|
+
// Precompute RGB interpolation helper
|
|
54
|
+
const interpolatePhaseColor = (dist, rippleIntensity = 0) => {
|
|
55
|
+
// Base phase colors based on distance and breath
|
|
56
|
+
const phase = dist * 0.5 - (tick * 0.05);
|
|
57
|
+
const val = (Math.sin(phase) + 1) / 2; // 0 to 1
|
|
58
|
+
|
|
59
|
+
// We will interpolate between dim and primary
|
|
60
|
+
// Then add ripple brightness
|
|
61
|
+
|
|
62
|
+
// parse values from RGB tuples string for speed, or just use hardcoded mapped values
|
|
63
|
+
// Primary: 180, 180, 255
|
|
64
|
+
// Dim: 60, 60, 90
|
|
65
|
+
|
|
66
|
+
let red = 60 + (120 * val);
|
|
67
|
+
let green = 60 + (120 * val);
|
|
68
|
+
let blue = 90 + (165 * val);
|
|
69
|
+
|
|
70
|
+
if (rippleIntensity > 0) {
|
|
71
|
+
// Add white peak to the ripple
|
|
72
|
+
red += (255 - red) * rippleIntensity;
|
|
73
|
+
green += (255 - green) * rippleIntensity;
|
|
74
|
+
blue += (255 - blue) * rippleIntensity;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `\x1b[38;2;${Math.floor(red)};${Math.floor(green)};${Math.floor(blue)}m`;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (let r = 0; r < playAreaH; r++) {
|
|
81
|
+
let rowStr = '';
|
|
82
|
+
let currColor = P.bg;
|
|
83
|
+
let currRun = '';
|
|
84
|
+
|
|
85
|
+
for (let c = 0; c < cols; c++) {
|
|
86
|
+
// Terminal fonts are roughly 2:1 aspect ratio
|
|
87
|
+
const dx = (c - cx) / 2;
|
|
88
|
+
const dy = (r - cy);
|
|
89
|
+
const dist = Math.sqrt(dx*dx + dy*dy);
|
|
90
|
+
|
|
91
|
+
let char = ' ';
|
|
92
|
+
let color = P.bg;
|
|
93
|
+
|
|
94
|
+
// Check ripples
|
|
95
|
+
let rippleIntensity = 0;
|
|
96
|
+
for (const rip of activeRipples) {
|
|
97
|
+
if (rip.radius > 0) {
|
|
98
|
+
const rDist = Math.abs(dist - rip.radius);
|
|
99
|
+
if (rDist < 1.5) {
|
|
100
|
+
// Sharp peak
|
|
101
|
+
rippleIntensity = Math.max(rippleIntensity, rip.life * (1 - (rDist/1.5)));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (dist < baseRadius || rippleIntensity > 0.1 || (dist > baseRadius && dist < baseRadius + 12 && Math.sin(dist*2 - tick*0.1) > 0.5)) {
|
|
107
|
+
char = '·';
|
|
108
|
+
|
|
109
|
+
if (dist < baseRadius - 1) {
|
|
110
|
+
char = '█';
|
|
111
|
+
} else if (dist < baseRadius) {
|
|
112
|
+
char = '▓';
|
|
113
|
+
} else if (dist < baseRadius + 1) {
|
|
114
|
+
char = '▒';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (rippleIntensity > 0.5) char = '█';
|
|
118
|
+
else if (rippleIntensity > 0.2) char = '▓';
|
|
119
|
+
|
|
120
|
+
color = P.bg + interpolatePhaseColor(dist, rippleIntensity);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (color !== currColor) {
|
|
124
|
+
rowStr += currColor + currRun;
|
|
125
|
+
currColor = color;
|
|
126
|
+
currRun = char;
|
|
127
|
+
} else {
|
|
128
|
+
currRun += char;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
rowStr += currColor + currRun + RESET;
|
|
132
|
+
lines[r] = rowStr;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── UI Overlays ──
|
|
136
|
+
const titleLine = `${P.bg}${P.secondary}Z E N M O D E${RESET}`;
|
|
137
|
+
lines[1] = `${P.bg}${centerText(titleLine, cols)}${RESET}`;
|
|
138
|
+
|
|
139
|
+
const subtitleLine = `${P.bg}${P.dim}Take a deep breath. Focus on your flow.${RESET}`;
|
|
140
|
+
lines[3] = `${P.bg}${centerText(subtitleLine, cols)}${RESET}`;
|
|
141
|
+
|
|
142
|
+
// UNVOID Button
|
|
143
|
+
const buttonRow = playAreaH + 1;
|
|
144
|
+
const button = renderUnvoidButton(cols, buttonRow, tick);
|
|
145
|
+
for (let i = 0; i < button.lines.length; i++) {
|
|
146
|
+
if (buttonRow + i < rows - statusHeight) {
|
|
147
|
+
lines[buttonRow + i] = `${P.bg}${button.lines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Status Bar
|
|
152
|
+
const statusLines = renderStatusBar(cols, { ...state, skinName: 'zen' }, tick);
|
|
153
|
+
const statusStartRow = rows - statusLines.length;
|
|
154
|
+
for (let i = 0; i < statusLines.length; i++) {
|
|
155
|
+
if (statusStartRow + i < rows) {
|
|
156
|
+
lines[statusStartRow + i] = `${P.bg}${statusLines[i].replace(/\x1b\[0m/g, RESET + P.bg)}${RESET}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
lines,
|
|
162
|
+
buttonRegion: {
|
|
163
|
+
x: button.x,
|
|
164
|
+
y: buttonRow + 1,
|
|
165
|
+
width: button.width,
|
|
166
|
+
height: button.height,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// ─── Unlock Sequence ──────────────────────────────────────────────────
|
|
2
|
+
// A cinematic glitch-out and power-down sequence transitioning the
|
|
3
|
+
// user back to the terminal prompt.
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { RESET, BOLD } from '../utils/colors.js';
|
|
7
|
+
import { centerText } from '../utils/terminal.js';
|
|
8
|
+
|
|
9
|
+
const GLITCH_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()_+-=[]{}|;:\'<>,.?/';
|
|
10
|
+
|
|
11
|
+
export function renderUnlockSequence(cols, rows, tick, unlockTick) {
|
|
12
|
+
const elapsed = tick - unlockTick;
|
|
13
|
+
const frames = 60; // 1 second exactly
|
|
14
|
+
|
|
15
|
+
const progress = Math.min(elapsed / frames, 1.0);
|
|
16
|
+
|
|
17
|
+
const lines = [];
|
|
18
|
+
|
|
19
|
+
// Phase 1 (0 -> 0.3): White noise static glitch
|
|
20
|
+
// Phase 2 (0.3 -> 0.6): Curtain drop (black screen wiping down)
|
|
21
|
+
// Phase 3 (0.6 -> 1.0): "SESSION UNLOCKED" text blooming in center
|
|
22
|
+
|
|
23
|
+
const cx = Math.floor(cols / 2);
|
|
24
|
+
const cy = Math.floor(rows / 2);
|
|
25
|
+
|
|
26
|
+
if (progress < 0.3) {
|
|
27
|
+
// White noise static glitch
|
|
28
|
+
const intensity = 1 - (progress / 0.3); // 1 to 0
|
|
29
|
+
for (let r = 0; r < rows; r++) {
|
|
30
|
+
let line = '';
|
|
31
|
+
for (let c = 0; c < cols; c++) {
|
|
32
|
+
if (Math.random() < intensity * 0.4) {
|
|
33
|
+
const char = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
|
|
34
|
+
// Random green, cyan, or white
|
|
35
|
+
const isGreen = Math.random() > 0.5;
|
|
36
|
+
line += isGreen ? `\x1b[38;2;0;255;150m${char}\x1b[0m` : `\x1b[38;2;150;255;255m${char}\x1b[0m`;
|
|
37
|
+
} else {
|
|
38
|
+
line += ' ';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lines.push(line);
|
|
42
|
+
}
|
|
43
|
+
} else if (progress < 0.6) {
|
|
44
|
+
// Curtain drop wipe (top to bottom fade to black)
|
|
45
|
+
const dropProgress = (progress - 0.3) / 0.3;
|
|
46
|
+
const curtainRow = Math.floor(rows * dropProgress);
|
|
47
|
+
|
|
48
|
+
for (let r = 0; r < rows; r++) {
|
|
49
|
+
if (r > curtainRow) {
|
|
50
|
+
// Still some sparse glitching below the curtain
|
|
51
|
+
let line = '';
|
|
52
|
+
for (let c = 0; c < cols; c++) {
|
|
53
|
+
if (Math.random() < 0.05) {
|
|
54
|
+
const char = GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
|
|
55
|
+
line += `\x1b[38;5;${235 + Math.floor(Math.random() * 8)}m${char}\x1b[0m`;
|
|
56
|
+
} else {
|
|
57
|
+
line += ' ';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push(line);
|
|
61
|
+
} else {
|
|
62
|
+
// Pure black void above the curtain
|
|
63
|
+
lines.push(' '.repeat(cols));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Final bloom: Just the UNLOCKED text in the middle
|
|
68
|
+
const textProgress = (progress - 0.6) / 0.4;
|
|
69
|
+
|
|
70
|
+
for (let r = 0; r < rows; r++) {
|
|
71
|
+
lines.push(' '.repeat(cols));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Box height expands
|
|
75
|
+
const boxHeight = Math.floor(7 * textProgress);
|
|
76
|
+
if (boxHeight > 0) {
|
|
77
|
+
const top = cy - Math.floor(boxHeight / 2);
|
|
78
|
+
const bot = cy + Math.floor(boxHeight / 2);
|
|
79
|
+
|
|
80
|
+
for (let r = top; r <= bot; r++) {
|
|
81
|
+
if (r >= 0 && r < rows) {
|
|
82
|
+
// Draw a sleek frame
|
|
83
|
+
const width = Math.floor(cols * 0.4 * textProgress);
|
|
84
|
+
const str = `\x1b[48;2;10;15;20m${' '.repeat(width)}\x1b[0m`;
|
|
85
|
+
lines[r] = centerText(str, cols);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (boxHeight > 2) {
|
|
90
|
+
const width = Math.floor(cols * 0.4 * textProgress);
|
|
91
|
+
const pulse = Math.floor(100 + 155 * textProgress);
|
|
92
|
+
const headerColor = `\x1b[38;2;0;${pulse};${Math.floor(pulse * 0.6)}m`;
|
|
93
|
+
|
|
94
|
+
const text = ' [ SYSTEM UNLOCKED ] ';
|
|
95
|
+
const padTotal = Math.max(0, width - text.length);
|
|
96
|
+
const padStr = ' '.repeat(Math.floor(padTotal / 2));
|
|
97
|
+
|
|
98
|
+
const innerBg = `\x1b[48;2;10;15;20m${padStr}${headerColor}${BOLD}${text}\x1b[0m\x1b[48;2;10;15;20m${' '.repeat(padTotal - Math.floor(padTotal / 2))}\x1b[0m`;
|
|
99
|
+
lines[cy] = centerText(innerBg, cols);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return lines;
|
|
105
|
+
}
|