novac 2.3.0 → 2.4.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/kits/kitarcade/kitdef.js +2709 -0
- package/kits/kitascii/kitdef.js +839 -0
- package/kits/kitlife/kitdef.js +575 -0
- package/kits/kitpet/kitdef.js +752 -0
- package/package.json +1 -1
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* kitascii — ASCII / ANSI art toolkit
|
|
5
|
+
*
|
|
6
|
+
* Four tools in one kit:
|
|
7
|
+
*
|
|
8
|
+
* kitdef.TextArt(opts) Big ASCII-font text renderer (figlet-style)
|
|
9
|
+
* kitdef.Animate(opts) Preset ASCII animations (fire, matrix, starfield, plasma, rain)
|
|
10
|
+
* kitdef.BoxArt(opts) Interactive box / border / banner composer
|
|
11
|
+
* kitdef.ImageToAscii(px) Convert a 2-D pixel array to ASCII art (for use with sharp etc.)
|
|
12
|
+
*
|
|
13
|
+
* All four share the kitarcade API:
|
|
14
|
+
* .start() .input(key) .tick(dt) .frame() .state .score
|
|
15
|
+
*
|
|
16
|
+
* Quick-start (animation):
|
|
17
|
+
* const { kitdef } = require('./kitascii');
|
|
18
|
+
* const { RunGame } = require('./run_game');
|
|
19
|
+
* RunGame(kitdef.Animate({ preset: 'matrix' }));
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// ─── ANSI / CANVAS ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const FC = {
|
|
25
|
+
blk:30,red:31,grn:32,yel:33,blu:34,mag:35,cyn:36,wht:37,
|
|
26
|
+
BLK:90,RED:91,GRN:92,YEL:93,BLU:94,MAG:95,CYN:96,WHT:97,
|
|
27
|
+
};
|
|
28
|
+
const BC = {
|
|
29
|
+
blk:40,red:41,grn:42,yel:43,blu:44,mag:45,cyn:46,wht:47,
|
|
30
|
+
BLK:100,RED:101,GRN:102,YEL:103,BLU:104,MAG:105,CYN:106,WHT:107,
|
|
31
|
+
};
|
|
32
|
+
const RST = '\x1b[0m';
|
|
33
|
+
const BLD = '\x1b[1m';
|
|
34
|
+
const DIM = '\x1b[2m';
|
|
35
|
+
|
|
36
|
+
function makeCanvas(W, H) {
|
|
37
|
+
const ch = Array.from({length:H}, () => new Array(W).fill(' '));
|
|
38
|
+
const fg = Array.from({length:H}, () => new Array(W).fill(FC.wht));
|
|
39
|
+
const bg = Array.from({length:H}, () => new Array(W).fill(BC.blk));
|
|
40
|
+
const cv = {
|
|
41
|
+
W, H,
|
|
42
|
+
clear(c=' ', f=FC.wht, b=BC.blk) {
|
|
43
|
+
for (let y=0;y<H;y++) for (let x=0;x<W;x++) { ch[y][x]=c;fg[y][x]=f;bg[y][x]=b; }
|
|
44
|
+
},
|
|
45
|
+
set(x, y, c, f=FC.wht, b=BC.blk) {
|
|
46
|
+
if (x<0||x>=W||y<0||y>=H) return;
|
|
47
|
+
ch[y][x]=(String(c)[0])||' '; fg[y][x]=f; bg[y][x]=b;
|
|
48
|
+
},
|
|
49
|
+
put(x, y, s, f=FC.wht, b=BC.blk) {
|
|
50
|
+
for (let i=0;i<s.length;i++) cv.set(x+i, y, s[i], f, b);
|
|
51
|
+
},
|
|
52
|
+
fill(x, y, w, h, c, f=FC.wht, b=BC.blk) {
|
|
53
|
+
for (let dy=0;dy<h;dy++) for (let dx=0;dx<w;dx++) cv.set(x+dx,y+dy,c,f,b);
|
|
54
|
+
},
|
|
55
|
+
get(x, y) {
|
|
56
|
+
if (x<0||x>=W||y<0||y>=H) return [' ', FC.wht, BC.blk];
|
|
57
|
+
return [ch[y][x], fg[y][x], bg[y][x]];
|
|
58
|
+
},
|
|
59
|
+
render() {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (let y=0;y<H;y++) {
|
|
62
|
+
let row='', cf=-1, cb=-1;
|
|
63
|
+
for (let x=0;x<W;x++) {
|
|
64
|
+
const f=fg[y][x], b=bg[y][x];
|
|
65
|
+
if (f!==cf||b!==cb) { row+=`\x1b[${f};${b}m`; cf=f; cb=b; }
|
|
66
|
+
row+=ch[y][x];
|
|
67
|
+
}
|
|
68
|
+
lines.push(row+RST);
|
|
69
|
+
}
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return cv;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeEmitter() {
|
|
77
|
+
const L = new Map();
|
|
78
|
+
return {
|
|
79
|
+
on(ev,fn) { if(!L.has(ev)) L.set(ev,[]); L.get(ev).push(fn); },
|
|
80
|
+
off(ev,fn) { if(L.has(ev)) L.set(ev,L.get(ev).filter(f=>f!==fn)); },
|
|
81
|
+
emit(ev,...a) { (L.get(ev)??[]).forEach(fn=>fn(...a)); },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rng = (n) => Math.random() * n | 0;
|
|
86
|
+
const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
|
|
87
|
+
|
|
88
|
+
// ─── FONT DATA ───────────────────────────────────────────────────────────────
|
|
89
|
+
// 5-row tall block font. Each character is an array of 5 strings (rows).
|
|
90
|
+
// Width varies per character.
|
|
91
|
+
|
|
92
|
+
const FONT = {
|
|
93
|
+
' ': [' ',' ',' ',' ',' '],
|
|
94
|
+
'A': [' ██ ','█ █','████','█ █','█ █'],
|
|
95
|
+
'B': ['███ ','█ █','███ ','█ █','███ '],
|
|
96
|
+
'C': [' ███','█ ','█ ','█ ',' ███'],
|
|
97
|
+
'D': ['██ ','█ █ ','█ █','█ █','███ '],
|
|
98
|
+
'E': ['████','█ ','███ ','█ ','████'],
|
|
99
|
+
'F': ['████','█ ','███ ','█ ','█ '],
|
|
100
|
+
'G': [' ███','█ ','█ ██','█ █',' ███'],
|
|
101
|
+
'H': ['█ █','█ █','████','█ █','█ █'],
|
|
102
|
+
'I': ['███',' █ ',' █ ',' █ ','███'],
|
|
103
|
+
'J': [' █',' █',' █','█ █',' █ '],
|
|
104
|
+
'K': ['█ █','█ █ ','██ ','█ █ ','█ █'],
|
|
105
|
+
'L': ['█ ','█ ','█ ','█ ','████'],
|
|
106
|
+
'M': ['█ █','██ ██','█ █ █','█ █','█ █'],
|
|
107
|
+
'N': ['█ █','██ █','█ █ █','█ ██','█ █'],
|
|
108
|
+
'O': [' ██ ','█ █','█ █','█ █',' ██ '],
|
|
109
|
+
'P': ['███ ','█ █','███ ','█ ','█ '],
|
|
110
|
+
'Q': [' ██ ','█ █','█ █','█ ██',' ███'],
|
|
111
|
+
'R': ['███ ','█ █','███ ','█ █ ','█ █'],
|
|
112
|
+
'S': [' ███','█ ',' ██ ',' █','███ '],
|
|
113
|
+
'T': ['████',' █ ',' █ ',' █ ',' █ '],
|
|
114
|
+
'U': ['█ █','█ █','█ █','█ █',' ██ '],
|
|
115
|
+
'V': ['█ █','█ █','█ █',' █ █ ',' █ '],
|
|
116
|
+
'W': ['█ █','█ █','█ █ █','██ ██','█ █'],
|
|
117
|
+
'X': ['█ █',' ██ ',' ██',' ██ ','█ █'], // fixed
|
|
118
|
+
'Y': ['█ █','█ █',' ██ ',' █ ',' █ '],
|
|
119
|
+
'Z': ['████',' █ ',' █ ','█ ','████'],
|
|
120
|
+
'0': [' ██ ','█ ██','██ █','█ █',' ██ '],
|
|
121
|
+
'1': [' █ ','██ ',' █ ',' █ ','███'],
|
|
122
|
+
'2': [' ██ ',' █',' ██ ','█ ','████'],
|
|
123
|
+
'3': ['███ ',' █',' ██ ',' █','███ '],
|
|
124
|
+
'4': ['█ █','█ █','████',' █',' █'],
|
|
125
|
+
'5': ['████','█ ','███ ',' █','███ '],
|
|
126
|
+
'6': [' ██ ','█ ','███ ','█ █',' ██ '],
|
|
127
|
+
'7': ['████',' █',' █ ',' █ ',' █ '],
|
|
128
|
+
'8': [' ██ ','█ █',' ██ ','█ █',' ██ '],
|
|
129
|
+
'9': [' ██ ','█ █',' ███',' █',' ██ '],
|
|
130
|
+
'!': [' █ ',' █ ',' █ ',' ',' █ '],
|
|
131
|
+
'?': [' ██ ',' █',' ██ ',' ',' █ '],
|
|
132
|
+
'.': [' ',' ',' ',' ',' █'],
|
|
133
|
+
',': [' ',' ',' ',' █','█ '],
|
|
134
|
+
':': [' █',' ',' █',' ',' '],
|
|
135
|
+
'-': [' ',' ','███',' ',' '],
|
|
136
|
+
'+': [' ',' █ ','███',' █ ',' '],
|
|
137
|
+
'/': [' █',' █ ',' █ ','█ ',' '],
|
|
138
|
+
'(': [' █','█ ','█ ','█ ',' █'],
|
|
139
|
+
')': ['█ ',' █',' █',' █','█ '],
|
|
140
|
+
'#': [' █ █ ',' ████',' █ █ ',' ████',' █ █ '],
|
|
141
|
+
'@': [' ███ ','█ █','█ ██ ','█ ',' ███ '],
|
|
142
|
+
'*': [' ','█ █ █',' ███ ','█ █ █',' '],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// ─── TEXT ART ─────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function makeTextArt(opts = {}) {
|
|
148
|
+
const em = makeEmitter();
|
|
149
|
+
const W = 80, H = 24;
|
|
150
|
+
|
|
151
|
+
let text, colorIdx, styleIdx, animTick, state, score;
|
|
152
|
+
let inputBuf, scrollX;
|
|
153
|
+
|
|
154
|
+
const COLORS = [
|
|
155
|
+
[FC.YEL, FC.yel],
|
|
156
|
+
[FC.CYN, FC.cyn],
|
|
157
|
+
[FC.GRN, FC.grn],
|
|
158
|
+
[FC.MAG, FC.mag],
|
|
159
|
+
[FC.RED, FC.red],
|
|
160
|
+
[FC.WHT, FC.BLK],
|
|
161
|
+
[FC.BLU, FC.blu],
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const STYLES = ['solid','shadow','outline','rainbow','wave','neon'];
|
|
165
|
+
|
|
166
|
+
const PRESETS = [
|
|
167
|
+
'HELLO', 'NOVA', 'KITASCII', 'ARCADE', 'PURCE',
|
|
168
|
+
'12345', 'CODE IS ART', 'HACK THE PLANET',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
function renderText(str, style, colors, tick) {
|
|
172
|
+
const rows = [[], [], [], [], []];
|
|
173
|
+
const chars = [...str.toUpperCase()];
|
|
174
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
175
|
+
const ch = chars[ci];
|
|
176
|
+
const glyph = FONT[ch] ?? FONT[' '];
|
|
177
|
+
for (let row = 0; row < 5; row++) {
|
|
178
|
+
rows[row].push({ glyphRow: glyph[row], charIdx: ci });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return rows;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function start() {
|
|
185
|
+
text = opts.text ?? PRESETS[rng(PRESETS.length)];
|
|
186
|
+
colorIdx = 0;
|
|
187
|
+
styleIdx = 0;
|
|
188
|
+
animTick = 0;
|
|
189
|
+
scrollX = 0;
|
|
190
|
+
state = 'playing';
|
|
191
|
+
score = 0;
|
|
192
|
+
inputBuf = text;
|
|
193
|
+
em.emit('start');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function input(key) {
|
|
197
|
+
if (key === 'escape') { return; }
|
|
198
|
+
if (key === 'up') { colorIdx = (colorIdx + 1) % COLORS.length; return; }
|
|
199
|
+
if (key === 'down') { colorIdx = (colorIdx - 1 + COLORS.length) % COLORS.length; return; }
|
|
200
|
+
if (key === 'left') { styleIdx = (styleIdx - 1 + STYLES.length) % STYLES.length; return; }
|
|
201
|
+
if (key === 'right') { styleIdx = (styleIdx + 1) % STYLES.length; return; }
|
|
202
|
+
if (key === 'tab') { text = PRESETS[(PRESETS.indexOf(text)+1)%PRESETS.length]; inputBuf=text; return; }
|
|
203
|
+
if (key === 'backspace') { inputBuf = inputBuf.slice(0,-1); text=inputBuf; return; }
|
|
204
|
+
if (key === 'enter') { text = inputBuf || 'HELLO'; return; }
|
|
205
|
+
if (key.length === 1 && inputBuf.length < 16) { inputBuf += key.toUpperCase(); text=inputBuf; }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function tick(dt=16) {
|
|
209
|
+
animTick += dt;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function frame() {
|
|
213
|
+
const cv = makeCanvas(W, H);
|
|
214
|
+
cv.clear(' ', FC.wht, BC.blk);
|
|
215
|
+
|
|
216
|
+
const style = STYLES[styleIdx];
|
|
217
|
+
const colors = COLORS[colorIdx];
|
|
218
|
+
const t = animTick / 100;
|
|
219
|
+
|
|
220
|
+
// compute rendered glyph rows
|
|
221
|
+
const str = (text || ' ').toUpperCase();
|
|
222
|
+
const chars = [...str];
|
|
223
|
+
const glyphs = chars.map(c => FONT[c] ?? FONT[' ']);
|
|
224
|
+
|
|
225
|
+
// measure total width
|
|
226
|
+
let totalW = 0;
|
|
227
|
+
for (const g of glyphs) totalW += (g[0]?.length ?? 3) + 1;
|
|
228
|
+
|
|
229
|
+
const startX = Math.max(0, (W - totalW) >> 1);
|
|
230
|
+
const startY = 5;
|
|
231
|
+
|
|
232
|
+
let cx = startX;
|
|
233
|
+
for (let ci = 0; ci < glyphs.length; ci++) {
|
|
234
|
+
const glyph = glyphs[ci];
|
|
235
|
+
const gw = glyph[0]?.length ?? 3;
|
|
236
|
+
|
|
237
|
+
for (let row = 0; row < 5; row++) {
|
|
238
|
+
const glyphRow = glyph[row] ?? '';
|
|
239
|
+
for (let gc = 0; gc < glyphRow.length; gc++) {
|
|
240
|
+
const ch = glyphRow[gc];
|
|
241
|
+
if (ch === ' ' || ch === '') continue;
|
|
242
|
+
|
|
243
|
+
const x = cx + gc;
|
|
244
|
+
const y = startY + row;
|
|
245
|
+
|
|
246
|
+
let col;
|
|
247
|
+
if (style === 'rainbow') {
|
|
248
|
+
const hue = (ci / glyphs.length + t * 0.2) % 1;
|
|
249
|
+
const palette = [FC.RED, FC.YEL, FC.GRN, FC.CYN, FC.BLU, FC.MAG];
|
|
250
|
+
col = palette[Math.floor(hue * palette.length)];
|
|
251
|
+
} else if (style === 'wave') {
|
|
252
|
+
const wave = Math.sin(t + ci * 0.8 + row * 0.3);
|
|
253
|
+
col = wave > 0.3 ? colors[0] : wave > -0.3 ? FC.WHT : colors[1];
|
|
254
|
+
} else if (style === 'neon') {
|
|
255
|
+
const pulse = Math.sin(t * 2 + ci * 0.5) > 0 ? colors[0] : FC.WHT;
|
|
256
|
+
col = pulse;
|
|
257
|
+
} else {
|
|
258
|
+
col = colors[row < 2 ? 0 : 1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (style === 'shadow') {
|
|
262
|
+
cv.set(x+1, y+1, '░', FC.BLK, BC.blk);
|
|
263
|
+
}
|
|
264
|
+
if (style === 'outline') {
|
|
265
|
+
for (const [dx,dy] of [[-1,0],[1,0],[0,-1],[0,1]]) {
|
|
266
|
+
cv.set(x+dx, y+dy, '▒', FC.BLK, BC.blk);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
cv.set(x, y, '█', col, BC.blk);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
cx += gw + 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// decorative border
|
|
277
|
+
const borderStyle = ['┌','─','┐','│','└','┘'][0];
|
|
278
|
+
cv.put(0, 0, '╔' + '═'.repeat(W-2) + '╗', FC.BLK, BC.blk);
|
|
279
|
+
cv.put(0, H-1, '╚' + '═'.repeat(W-2) + '╝', FC.BLK, BC.blk);
|
|
280
|
+
for (let y=1; y<H-1; y++) { cv.set(0,y,'║',FC.BLK,BC.blk); cv.set(W-1,y,'║',FC.BLK,BC.blk); }
|
|
281
|
+
|
|
282
|
+
// controls
|
|
283
|
+
cv.put(2, 1, ` TEXT ART ↑↓ color ←→ style TAB preset Type to edit ENTER set`, FC.YEL, BC.blk);
|
|
284
|
+
cv.put(2, 2, ` Style: ${STYLES[styleIdx].padEnd(10)} Color: ${colorIdx+1}/${COLORS.length} Text: "${text}"`, FC.BLK, BC.blk);
|
|
285
|
+
|
|
286
|
+
// input display
|
|
287
|
+
const cursor = Math.floor(animTick/500)%2 === 0 ? '▌' : ' ';
|
|
288
|
+
cv.put(2, H-3, ` > ${inputBuf}${cursor}`.padEnd(W-3,' '), FC.CYN, BC.blk);
|
|
289
|
+
cv.put(2, H-2, ` BACKSPACE delete ENTER render TAB next preset`, FC.BLK, BC.blk);
|
|
290
|
+
|
|
291
|
+
return cv.render();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { start, input, tick, frame,
|
|
295
|
+
on: em.on.bind(em), off: em.off.bind(em),
|
|
296
|
+
get state() { return state; }, get score() { return score; },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── ANIMATIONS ──────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
function makeAnimate(opts = {}) {
|
|
303
|
+
const em = makeEmitter();
|
|
304
|
+
const W = opts.width ?? 80;
|
|
305
|
+
const H = opts.height ?? 24;
|
|
306
|
+
|
|
307
|
+
const PRESETS = ['matrix','fire','starfield','plasma','rain','snow','worms'];
|
|
308
|
+
let presetIdx, t, state, score, speed;
|
|
309
|
+
|
|
310
|
+
// ── per-preset state ──
|
|
311
|
+
let matrix_cols, fire_buf, stars, worms;
|
|
312
|
+
|
|
313
|
+
function initPreset(name) {
|
|
314
|
+
t = 0;
|
|
315
|
+
if (name === 'matrix') {
|
|
316
|
+
// each column: { y (head position), speed, chars[] }
|
|
317
|
+
const KATAKANA = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
|
|
318
|
+
matrix_cols = Array.from({length: W}, (_, x) => ({
|
|
319
|
+
y: -rng(H),
|
|
320
|
+
speed: 0.3 + Math.random() * 0.7,
|
|
321
|
+
chars: Array.from({length: H+5}, () => KATAKANA[rng(KATAKANA.length)]),
|
|
322
|
+
bright: rng(H),
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
if (name === 'fire') {
|
|
326
|
+
fire_buf = new Float32Array((W) * (H+2)).fill(0);
|
|
327
|
+
}
|
|
328
|
+
if (name === 'starfield') {
|
|
329
|
+
stars = Array.from({length: 120}, () => ({
|
|
330
|
+
x: (Math.random() * 2 - 1),
|
|
331
|
+
y: (Math.random() * 2 - 1),
|
|
332
|
+
z: Math.random(),
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
if (name === 'worms') {
|
|
336
|
+
worms = Array.from({length: 8}, (_, i) => ({
|
|
337
|
+
x: rng(W), y: rng(H),
|
|
338
|
+
dx: (rng(3)-1)||1, dy: (rng(3)-1)||1,
|
|
339
|
+
col: [FC.GRN,FC.CYN,FC.MAG,FC.YEL,FC.RED,FC.BLU,FC.WHT,FC.grn][i],
|
|
340
|
+
tail: [],
|
|
341
|
+
tailLen: 12 + rng(20),
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function start() {
|
|
347
|
+
presetIdx = PRESETS.indexOf(opts.preset ?? 'matrix');
|
|
348
|
+
if (presetIdx < 0) presetIdx = 0;
|
|
349
|
+
speed = opts.speed ?? 1;
|
|
350
|
+
state = 'running';
|
|
351
|
+
score = 0;
|
|
352
|
+
initPreset(PRESETS[presetIdx]);
|
|
353
|
+
em.emit('start');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function input(key) {
|
|
357
|
+
if (key === 'left' || key === 'p') {
|
|
358
|
+
presetIdx = (presetIdx - 1 + PRESETS.length) % PRESETS.length;
|
|
359
|
+
initPreset(PRESETS[presetIdx]);
|
|
360
|
+
}
|
|
361
|
+
if (key === 'right' || key === 'n') {
|
|
362
|
+
presetIdx = (presetIdx + 1) % PRESETS.length;
|
|
363
|
+
initPreset(PRESETS[presetIdx]);
|
|
364
|
+
}
|
|
365
|
+
if (key === 'space') { state = state === 'running' ? 'paused' : 'running'; }
|
|
366
|
+
if (key === '+' || key === '=') speed = clamp(speed + 0.2, 0.2, 3);
|
|
367
|
+
if (key === '-') speed = clamp(speed - 0.2, 0.2, 3);
|
|
368
|
+
if (key === 'r') { initPreset(PRESETS[presetIdx]); }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function tick(dt = 16) {
|
|
372
|
+
if (state !== 'running') return;
|
|
373
|
+
t += dt * speed / 1000;
|
|
374
|
+
score++;
|
|
375
|
+
|
|
376
|
+
const name = PRESETS[presetIdx];
|
|
377
|
+
|
|
378
|
+
if (name === 'matrix') {
|
|
379
|
+
for (const col of matrix_cols) {
|
|
380
|
+
col.y += col.speed * speed;
|
|
381
|
+
if (col.y > H + 5) {
|
|
382
|
+
col.y = -rng(H) - 3;
|
|
383
|
+
col.speed = 0.3 + Math.random() * 0.7;
|
|
384
|
+
col.bright = rng(H);
|
|
385
|
+
// refresh chars
|
|
386
|
+
const KATAKANA = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
|
|
387
|
+
col.chars = Array.from({length: H+5}, () => KATAKANA[rng(KATAKANA.length)]);
|
|
388
|
+
}
|
|
389
|
+
// occasionally mutate a char
|
|
390
|
+
if (Math.random() < 0.05) {
|
|
391
|
+
const KATAKANA = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
|
|
392
|
+
col.chars[rng(col.chars.length)] = KATAKANA[rng(KATAKANA.length)];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (name === 'fire') {
|
|
398
|
+
// seed bottom row
|
|
399
|
+
for (let x=0; x<W; x++) {
|
|
400
|
+
fire_buf[(H+1)*W+x] = Math.random() < 0.6 ? 28 + rng(8) : 0;
|
|
401
|
+
}
|
|
402
|
+
// propagate upward
|
|
403
|
+
for (let y=H; y>=0; y--) {
|
|
404
|
+
for (let x=0; x<W; x++) {
|
|
405
|
+
const below = fire_buf[(y+1)*W+x];
|
|
406
|
+
const wind = rng(3) - 1;
|
|
407
|
+
const decay = rng(3);
|
|
408
|
+
const tx = clamp(x + wind, 0, W-1);
|
|
409
|
+
fire_buf[y*W+tx] = Math.max(0, below - decay);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (name === 'starfield') {
|
|
415
|
+
for (const s of stars) {
|
|
416
|
+
s.z -= 0.01 * speed;
|
|
417
|
+
if (s.z <= 0) {
|
|
418
|
+
s.x = (Math.random() * 2 - 1);
|
|
419
|
+
s.y = (Math.random() * 2 - 1);
|
|
420
|
+
s.z = 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (name === 'worms') {
|
|
426
|
+
for (const w of worms) {
|
|
427
|
+
w.tail.unshift([w.x, w.y]);
|
|
428
|
+
if (w.tail.length > w.tailLen) w.tail.pop();
|
|
429
|
+
// random direction change
|
|
430
|
+
if (Math.random() < 0.15) {
|
|
431
|
+
w.dx = (rng(3)-1);
|
|
432
|
+
w.dy = (rng(3)-1);
|
|
433
|
+
if (!w.dx && !w.dy) w.dx = 1;
|
|
434
|
+
}
|
|
435
|
+
w.x = clamp(w.x + w.dx, 0, W-1);
|
|
436
|
+
w.y = clamp(w.y + w.dy, 0, H-2);
|
|
437
|
+
if (w.x <= 0 || w.x >= W-1) w.dx = -w.dx;
|
|
438
|
+
if (w.y <= 0 || w.y >= H-2) w.dy = -w.dy;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function frame() {
|
|
444
|
+
const cv = makeCanvas(W, H);
|
|
445
|
+
cv.clear(' ', FC.wht, BC.blk);
|
|
446
|
+
|
|
447
|
+
const name = PRESETS[presetIdx];
|
|
448
|
+
|
|
449
|
+
// ── MATRIX ──
|
|
450
|
+
if (name === 'matrix') {
|
|
451
|
+
for (let x=0; x<W; x++) {
|
|
452
|
+
const col = matrix_cols[x];
|
|
453
|
+
const headY = Math.floor(col.y);
|
|
454
|
+
for (let y=0; y<H; y++) {
|
|
455
|
+
const dist = headY - y;
|
|
456
|
+
if (dist < 0 || dist > H) continue;
|
|
457
|
+
const charIdx = ((y - Math.floor(col.y - H)) + col.chars.length) % col.chars.length;
|
|
458
|
+
const ch = col.chars[charIdx] ?? '0';
|
|
459
|
+
if (dist === 0) {
|
|
460
|
+
cv.set(x, y, ch, FC.WHT, BC.blk); // bright head
|
|
461
|
+
} else if (dist < 3) {
|
|
462
|
+
cv.set(x, y, ch, FC.GRN, BC.blk);
|
|
463
|
+
} else if (dist < 8) {
|
|
464
|
+
cv.set(x, y, ch, FC.grn, BC.blk);
|
|
465
|
+
} else if (dist < 15) {
|
|
466
|
+
cv.set(x, y, ch, FC.BLK, BC.blk);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── FIRE ──
|
|
473
|
+
if (name === 'fire') {
|
|
474
|
+
const FIRE_CHARS = ' .\'`^",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$';
|
|
475
|
+
const FIRE_COLORS = [
|
|
476
|
+
FC.BLK, FC.red, FC.red, FC.RED, FC.RED,
|
|
477
|
+
FC.yel, FC.YEL, FC.YEL, FC.WHT, FC.WHT,
|
|
478
|
+
];
|
|
479
|
+
for (let y=0; y<H; y++) {
|
|
480
|
+
for (let x=0; x<W; x++) {
|
|
481
|
+
const v = fire_buf[y*W+x];
|
|
482
|
+
if (v <= 0) continue;
|
|
483
|
+
const vi = clamp(Math.floor(v), 0, FIRE_CHARS.length-1);
|
|
484
|
+
const ci = clamp(Math.floor(v / 35 * FIRE_COLORS.length), 0, FIRE_COLORS.length-1);
|
|
485
|
+
cv.set(x, y, FIRE_CHARS[vi], FIRE_COLORS[ci], BC.blk);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── STARFIELD ──
|
|
491
|
+
if (name === 'starfield') {
|
|
492
|
+
const STAR_CHARS = ['.', '+', '*', '✦', '★', '✸'];
|
|
493
|
+
for (const s of stars) {
|
|
494
|
+
const sx = Math.floor((s.x / s.z + 1) / 2 * W);
|
|
495
|
+
const sy = Math.floor((s.y / s.z + 1) / 2 * H);
|
|
496
|
+
if (sx < 0 || sx >= W || sy < 0 || sy >= H) continue;
|
|
497
|
+
const brightness = 1 - s.z;
|
|
498
|
+
const ci = Math.floor(brightness * (STAR_CHARS.length-1));
|
|
499
|
+
const col = brightness > 0.8 ? FC.WHT : brightness > 0.5 ? FC.BLK : FC.BLK;
|
|
500
|
+
cv.set(sx, sy, STAR_CHARS[ci], col, BC.blk);
|
|
501
|
+
}
|
|
502
|
+
// center crosshair
|
|
503
|
+
cv.put((W>>1)-1, H>>1, '─┼─', FC.BLK, BC.blk);
|
|
504
|
+
cv.set(W>>1, (H>>1)-1, '│', FC.BLK, BC.blk);
|
|
505
|
+
cv.set(W>>1, (H>>1)+1, '│', FC.BLK, BC.blk);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── PLASMA ──
|
|
509
|
+
if (name === 'plasma') {
|
|
510
|
+
const PLASMA_CHARS = ' ░▒▓█';
|
|
511
|
+
const palettes = [
|
|
512
|
+
[FC.red,FC.RED,FC.yel,FC.YEL,FC.WHT],
|
|
513
|
+
[FC.blu,FC.BLU,FC.cyn,FC.CYN,FC.WHT],
|
|
514
|
+
[FC.mag,FC.MAG,FC.red,FC.RED,FC.WHT],
|
|
515
|
+
[FC.grn,FC.GRN,FC.yel,FC.YEL,FC.WHT],
|
|
516
|
+
];
|
|
517
|
+
const pal = palettes[Math.floor(t*0.1) % palettes.length];
|
|
518
|
+
for (let y=0; y<H; y++) {
|
|
519
|
+
for (let x=0; x<W; x++) {
|
|
520
|
+
const v =
|
|
521
|
+
Math.sin(x / 6 + t) +
|
|
522
|
+
Math.sin(y / 3 + t * 0.7) +
|
|
523
|
+
Math.sin((x + y) / 9 + t * 0.5) +
|
|
524
|
+
Math.sin(Math.sqrt(((x-W/2)**2 + (y-H/2)**2)) / 4 + t);
|
|
525
|
+
const n = (v + 4) / 8;
|
|
526
|
+
const ci = clamp(Math.floor(n * PLASMA_CHARS.length), 0, PLASMA_CHARS.length-1);
|
|
527
|
+
const fc = pal[clamp(Math.floor(n * pal.length), 0, pal.length-1)];
|
|
528
|
+
cv.set(x, y, PLASMA_CHARS[ci], fc, BC.blk);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── RAIN ──
|
|
534
|
+
if (name === 'rain') {
|
|
535
|
+
// simple: re-derive each frame from t
|
|
536
|
+
for (let x=0; x<W; x++) {
|
|
537
|
+
const offset = Math.sin(x * 1.7) * 10 + x * 0.3;
|
|
538
|
+
for (let y=0; y<H; y++) {
|
|
539
|
+
const drop = (y + offset + t * 8) % H;
|
|
540
|
+
if (drop < 1) {
|
|
541
|
+
cv.set(x, y, '│', FC.CYN, BC.blk);
|
|
542
|
+
} else if (drop < 3) {
|
|
543
|
+
cv.set(x, y, '│', FC.BLU, BC.blk);
|
|
544
|
+
} else if (drop < 5) {
|
|
545
|
+
cv.set(x, y, '╵', FC.blu, BC.blk);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// puddle bottom
|
|
550
|
+
for (let x=0; x<W; x++) {
|
|
551
|
+
const splash = Math.sin(t * 3 + x * 0.5) > 0.8;
|
|
552
|
+
cv.set(x, H-1, splash ? '~' : '_', FC.BLU, BC.blk);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── SNOW ──
|
|
557
|
+
if (name === 'snow') {
|
|
558
|
+
for (let x=0; x<W; x++) {
|
|
559
|
+
for (let y=0; y<H; y++) {
|
|
560
|
+
const drift = Math.sin(t * 0.5 + x * 0.3 + y * 0.1) * 3;
|
|
561
|
+
const offset = x * 2.3 + y * 1.7 + drift;
|
|
562
|
+
const phase = (t * 2 + offset) % H;
|
|
563
|
+
if (phase < 0.8) {
|
|
564
|
+
const ch = ['*','·','❄','❅'][rng(4)];
|
|
565
|
+
cv.set(x, y, ch, FC.WHT, BC.blk);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// snow pile
|
|
570
|
+
for (let x=0; x<W; x++) {
|
|
571
|
+
const pile = 1 + Math.floor(Math.sin(x * 0.3) * 1 + 1);
|
|
572
|
+
for (let p=0; p<pile; p++) cv.set(x, H-1-p, '▓', FC.WHT, BC.blk);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ── WORMS ──
|
|
577
|
+
if (name === 'worms') {
|
|
578
|
+
for (const w of worms) {
|
|
579
|
+
for (let i=0; i<w.tail.length; i++) {
|
|
580
|
+
const [tx,ty] = w.tail[i];
|
|
581
|
+
const alpha = 1 - i / w.tail.length;
|
|
582
|
+
const ch = i===0 ? '●' : alpha > 0.5 ? '▪' : '·';
|
|
583
|
+
const col = i===0 ? FC.WHT : w.col;
|
|
584
|
+
cv.set(tx, ty, ch, col, BC.blk);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ── HUD ──
|
|
590
|
+
const hud = ` ${name.toUpperCase().padEnd(10)} ← → change SPACE pause +/- speed R reset`;
|
|
591
|
+
cv.put(0, 0, hud, FC.YEL, BC.blk);
|
|
592
|
+
|
|
593
|
+
return cv.render();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { start, input, tick, frame,
|
|
597
|
+
on: em.on.bind(em), off: em.off.bind(em),
|
|
598
|
+
get state() { return state; }, get score() { return score; },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ─── BOX ART ─────────────────────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
function makeBoxArt(opts = {}) {
|
|
605
|
+
const em = makeEmitter();
|
|
606
|
+
const W = 72, H = 28;
|
|
607
|
+
|
|
608
|
+
const BOX_STYLES = [
|
|
609
|
+
{ name:'Single', tl:'┌',tr:'┐',bl:'└',br:'┘',h:'─',v:'│',tee:'┬',bee:'┴',lef:'├',rig:'┤',crs:'┼' },
|
|
610
|
+
{ name:'Double', tl:'╔',tr:'╗',bl:'╚',br:'╝',h:'═',v:'║',tee:'╦',bee:'╩',lef:'╠',rig:'╣',crs:'╬' },
|
|
611
|
+
{ name:'Thick', tl:'┏',tr:'┓',bl:'┗',br:'┛',h:'━',v:'┃',tee:'┳',bee:'┻',lef:'┣',rig:'┫',crs:'╋' },
|
|
612
|
+
{ name:'Rounded', tl:'╭',tr:'╮',bl:'╰',br:'╯',h:'─',v:'│',tee:'┬',bee:'┴',lef:'├',rig:'┤',crs:'┼' },
|
|
613
|
+
{ name:'Ascii', tl:'+',tr:'+',bl:'+',br:'+',h:'-',v:'|',tee:'+',bee:'+',lef:'+',rig:'+',crs:'+' },
|
|
614
|
+
{ name:'Hash', tl:'#',tr:'#',bl:'#',br:'#',h:'#',v:'#',tee:'#',bee:'#',lef:'#',rig:'#',crs:'#' },
|
|
615
|
+
{ name:'Stars', tl:'*',tr:'*',bl:'*',br:'*',h:'*',v:'*',tee:'*',bee:'*',lef:'*',rig:'*',crs:'*' },
|
|
616
|
+
{ name:'Fancy', tl:'╓',tr:'╖',bl:'╙',br:'╜',h:'─',v:'║',tee:'╥',bee:'╨',lef:'╟',rig:'╢',crs:'╫' },
|
|
617
|
+
];
|
|
618
|
+
|
|
619
|
+
const FILLS = [' ', '░', '▒', '▓', '·', '∙', '•', '◦', '·'];
|
|
620
|
+
const FC_LIST = Object.values(FC);
|
|
621
|
+
|
|
622
|
+
let bx, by, bw, bh, styleIdx, fillIdx, fgIdx, bgIdx;
|
|
623
|
+
let title, inputBuf, inputMode;
|
|
624
|
+
let state, score;
|
|
625
|
+
|
|
626
|
+
function start() {
|
|
627
|
+
bx=4; by=4; bw=40; bh=14;
|
|
628
|
+
styleIdx=0; fillIdx=0; fgIdx=0; bgIdx=0;
|
|
629
|
+
title = opts.title ?? 'MY BOX';
|
|
630
|
+
inputBuf= ''; inputMode=null;
|
|
631
|
+
state='playing'; score=0;
|
|
632
|
+
em.emit('start');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function input(key) {
|
|
636
|
+
if (inputMode) {
|
|
637
|
+
if (key==='escape') { inputMode=null; inputBuf=''; return; }
|
|
638
|
+
if (key==='enter') { title=inputBuf; inputMode=null; inputBuf=''; return; }
|
|
639
|
+
if (key==='backspace') { inputBuf=inputBuf.slice(0,-1); return; }
|
|
640
|
+
if (key.length===1 && inputBuf.length<30) { inputBuf+=key; return; }
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// resize
|
|
644
|
+
if (key==='right') bw = clamp(bw+2, 6, W-bx-2);
|
|
645
|
+
if (key==='left') bw = clamp(bw-2, 6, W-bx-2);
|
|
646
|
+
if (key==='down') bh = clamp(bh+1, 4, H-by-2);
|
|
647
|
+
if (key==='up') bh = clamp(bh-1, 4, H-by-2);
|
|
648
|
+
// cycle options
|
|
649
|
+
if (key==='s') styleIdx = (styleIdx+1) % BOX_STYLES.length;
|
|
650
|
+
if (key==='f') fillIdx = (fillIdx+1) % FILLS.length;
|
|
651
|
+
if (key==='c') fgIdx = (fgIdx+1) % FC_LIST.length;
|
|
652
|
+
if (key==='b') bgIdx = (bgIdx+1) % Object.values(BC).length;
|
|
653
|
+
if (key==='t') { inputMode='title'; inputBuf=title; }
|
|
654
|
+
// move box
|
|
655
|
+
if (key==='w') by = clamp(by-1, 2, H-bh-2);
|
|
656
|
+
if (key==='a') bx = clamp(bx-1, 0, W-bw-2);
|
|
657
|
+
if (key==='z') by = clamp(by+1, 2, H-bh-2);
|
|
658
|
+
if (key==='x') bx = clamp(bx+1, 0, W-bw-2);
|
|
659
|
+
// export
|
|
660
|
+
if (key==='e') { score++; em.emit('score', score); }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function tick(dt=16) {}
|
|
664
|
+
|
|
665
|
+
function frame() {
|
|
666
|
+
const cv = makeCanvas(W, H);
|
|
667
|
+
cv.clear(' ', FC.wht, BC.blk);
|
|
668
|
+
|
|
669
|
+
cv.put(0, 0, ` BOX ART S=style F=fill C=fg-color B=bg-color T=title WASD/↑↓←→ move/resize E=export`, FC.YEL, BC.blk);
|
|
670
|
+
cv.put(0, 1, '─'.repeat(W), FC.BLK, BC.blk);
|
|
671
|
+
|
|
672
|
+
const style = BOX_STYLES[styleIdx];
|
|
673
|
+
const fill = FILLS[fillIdx];
|
|
674
|
+
const fgColor = FC_LIST[fgIdx];
|
|
675
|
+
const bgVals = Object.values(BC);
|
|
676
|
+
const bgColor = bgVals[bgIdx];
|
|
677
|
+
|
|
678
|
+
// fill interior
|
|
679
|
+
if (fillIdx > 0) {
|
|
680
|
+
cv.fill(bx+1, by+1, bw-2, bh-2, fill, fgColor, BC.blk);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// draw box
|
|
684
|
+
cv.set(bx, by, style.tl, fgColor, BC.blk);
|
|
685
|
+
cv.set(bx+bw-1, by, style.tr, fgColor, BC.blk);
|
|
686
|
+
cv.set(bx, by+bh-1, style.bl, fgColor, BC.blk);
|
|
687
|
+
cv.set(bx+bw-1, by+bh-1, style.br, fgColor, BC.blk);
|
|
688
|
+
for (let x=bx+1; x<bx+bw-1; x++) {
|
|
689
|
+
cv.set(x, by, style.h, fgColor, BC.blk);
|
|
690
|
+
cv.set(x, by+bh-1, style.h, fgColor, BC.blk);
|
|
691
|
+
}
|
|
692
|
+
for (let y=by+1; y<by+bh-1; y++) {
|
|
693
|
+
cv.set(bx, y, style.v, fgColor, BC.blk);
|
|
694
|
+
cv.set(bx+bw-1, y, style.v, fgColor, BC.blk);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// title
|
|
698
|
+
if (inputMode === 'title') {
|
|
699
|
+
const t = `[ ${inputBuf}▌ ]`;
|
|
700
|
+
const tx = bx + ((bw - t.length) >> 1);
|
|
701
|
+
cv.put(tx, by, t, FC.YEL, BC.blk);
|
|
702
|
+
} else if (title) {
|
|
703
|
+
const t = `[ ${title} ]`;
|
|
704
|
+
const tx = bx + ((bw - t.length) >> 1);
|
|
705
|
+
cv.put(tx, by, t, FC.YEL, BC.blk);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// info panel
|
|
709
|
+
const px = 1, py = H - 6;
|
|
710
|
+
cv.put(px, py, `Style: ${style.name.padEnd(10)} tl:${style.tl} h:${style.h} v:${style.v}`, FC.wht, BC.blk);
|
|
711
|
+
cv.put(px, py+1, `Fill: "${fill}"`, FC.wht, BC.blk);
|
|
712
|
+
cv.put(px, py+2, `Size: ${bw}×${bh} at ${bx},${by}`, FC.wht, BC.blk);
|
|
713
|
+
cv.put(px, py+3, `Exports: ${score}`, FC.BLK, BC.blk);
|
|
714
|
+
cv.put(px, py+4,
|
|
715
|
+
`\x1b[${fgColor}m████\x1b[0m FG \x1b[${bgColor}m████\x1b[0m BG`, FC.wht, BC.blk);
|
|
716
|
+
|
|
717
|
+
return cv.render();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return { start, input, tick, frame,
|
|
721
|
+
on: em.on.bind(em), off: em.off.bind(em),
|
|
722
|
+
get state() { return state; }, get score() { return score; },
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ─── IMAGE → ASCII ───────────────────────────────────────────────────────────
|
|
727
|
+
/**
|
|
728
|
+
* ImageToAscii(pixelGrid, opts)
|
|
729
|
+
*
|
|
730
|
+
* Converts a 2-D pixel grid to ASCII art string.
|
|
731
|
+
* pixelGrid: Array<Array<{r,g,b}>> (rows of pixels)
|
|
732
|
+
*
|
|
733
|
+
* opts:
|
|
734
|
+
* width: target character width (default 80)
|
|
735
|
+
* color: use ANSI colors (default true)
|
|
736
|
+
* charset: 'block'|'shade'|'ascii'|'braille' (default 'shade')
|
|
737
|
+
* invert: invert brightness (default false)
|
|
738
|
+
*
|
|
739
|
+
* Returns: ANSI string ready to write to stdout.
|
|
740
|
+
*
|
|
741
|
+
* Example with jimp:
|
|
742
|
+
* const Jimp = require('jimp');
|
|
743
|
+
* const { ImageToAscii } = require('./kitascii');
|
|
744
|
+
* const img = await Jimp.read('photo.jpg');
|
|
745
|
+
* img.resize(80, 40);
|
|
746
|
+
* const grid = [];
|
|
747
|
+
* img.scan(0,0,img.width,img.height,(x,y,idx)=>{
|
|
748
|
+
* if (!grid[y]) grid[y]=[];
|
|
749
|
+
* grid[y][x]={ r:img.bitmap.data[idx], g:img.bitmap.data[idx+1], b:img.bitmap.data[idx+2] };
|
|
750
|
+
* });
|
|
751
|
+
* console.log(ImageToAscii(grid, { color:true }));
|
|
752
|
+
*/
|
|
753
|
+
|
|
754
|
+
const CHARSETS = {
|
|
755
|
+
shade: ' ░▒▓█',
|
|
756
|
+
block: ' ▏▎▍▌▋▊▉█',
|
|
757
|
+
ascii: ' .\'`^",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$',
|
|
758
|
+
braille:'⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿',
|
|
759
|
+
minimal:' ·∙•●',
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
function ImageToAscii(pixelGrid, opts = {}) {
|
|
763
|
+
if (!pixelGrid || !pixelGrid.length) return '';
|
|
764
|
+
|
|
765
|
+
const charset = CHARSETS[opts.charset ?? 'shade'];
|
|
766
|
+
const color = opts.color ?? true;
|
|
767
|
+
const invert = opts.invert ?? false;
|
|
768
|
+
|
|
769
|
+
const srcH = pixelGrid.length;
|
|
770
|
+
const srcW = pixelGrid[0].length;
|
|
771
|
+
const tgtW = opts.width ?? 80;
|
|
772
|
+
// terminal chars are ~2:1 height:width, so scale height by 0.5
|
|
773
|
+
const tgtH = Math.round(tgtW * (srcH / srcW) * 0.5);
|
|
774
|
+
|
|
775
|
+
const lines = [];
|
|
776
|
+
|
|
777
|
+
for (let ty = 0; ty < tgtH; ty++) {
|
|
778
|
+
let line = '';
|
|
779
|
+
const sy = Math.floor(ty / tgtH * srcH);
|
|
780
|
+
const row = pixelGrid[clamp(sy, 0, srcH-1)];
|
|
781
|
+
|
|
782
|
+
for (let tx = 0; tx < tgtW; tx++) {
|
|
783
|
+
const sx = Math.floor(tx / tgtW * srcW);
|
|
784
|
+
const px = row[clamp(sx, 0, srcW-1)] ?? {r:0,g:0,b:0};
|
|
785
|
+
const {r, g, b} = px;
|
|
786
|
+
|
|
787
|
+
// luminance (perceptual)
|
|
788
|
+
const lum = 0.2126*r + 0.7152*g + 0.0722*b;
|
|
789
|
+
const n = invert ? 1 - lum/255 : lum/255;
|
|
790
|
+
const ci = clamp(Math.floor(n * charset.length), 0, charset.length-1);
|
|
791
|
+
const ch = charset[ci];
|
|
792
|
+
|
|
793
|
+
if (color) {
|
|
794
|
+
// find closest ANSI 8-color
|
|
795
|
+
const ansiCol = closestAnsi(r, g, b);
|
|
796
|
+
line += `\x1b[${ansiCol}m${ch}`;
|
|
797
|
+
} else {
|
|
798
|
+
line += ch;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
lines.push(line + RST);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return lines.join('\n');
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function closestAnsi(r, g, b) {
|
|
808
|
+
const ANSI_RGB = [
|
|
809
|
+
[0,0,0],[128,0,0],[0,128,0],[128,128,0],
|
|
810
|
+
[0,0,128],[128,0,128],[0,128,128],[192,192,192],
|
|
811
|
+
[128,128,128],[255,0,0],[0,255,0],[255,255,0],
|
|
812
|
+
[0,0,255],[255,0,255],[0,255,255],[255,255,255],
|
|
813
|
+
];
|
|
814
|
+
const FC_VALS = [30,31,32,33,34,35,36,37,90,91,92,93,94,95,96,97];
|
|
815
|
+
let best=Infinity, bestIdx=0;
|
|
816
|
+
for (let i=0;i<ANSI_RGB.length;i++) {
|
|
817
|
+
const [ar,ag,ab]=ANSI_RGB[i];
|
|
818
|
+
const d=(r-ar)**2+(g-ag)**2+(b-ab)**2;
|
|
819
|
+
if(d<best){best=d;bestIdx=i;}
|
|
820
|
+
}
|
|
821
|
+
return FC_VALS[bestIdx];
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ─── EXPORT ──────────────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
const kitdef = {
|
|
827
|
+
TextArt: (opts) => { const g = makeTextArt(opts); g.start(); return g; },
|
|
828
|
+
Animate: (opts) => { const g = makeAnimate(opts); g.start(); return g; },
|
|
829
|
+
BoxArt: (opts) => { const g = makeBoxArt(opts); g.start(); return g; },
|
|
830
|
+
|
|
831
|
+
// standalone function (not a game loop)
|
|
832
|
+
ImageToAscii,
|
|
833
|
+
|
|
834
|
+
// animation presets
|
|
835
|
+
animPresets: ['matrix','fire','starfield','plasma','rain','snow','worms'],
|
|
836
|
+
makeTextArt, makeAnimate, makeBoxArt, ImageToAscii
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
module.exports = { kitdef };
|