@meebs/meeb 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.
Files changed (89) hide show
  1. package/README.md +158 -0
  2. package/animals/alien.js +34 -0
  3. package/animals/axolotl.js +38 -0
  4. package/animals/bat.js +34 -0
  5. package/animals/bear.js +41 -0
  6. package/animals/bee.js +32 -0
  7. package/animals/bunny.js +45 -0
  8. package/animals/butterfly.js +36 -0
  9. package/animals/capybara.js +36 -0
  10. package/animals/cat.js +44 -0
  11. package/animals/chameleon.js +44 -0
  12. package/animals/chick.js +27 -0
  13. package/animals/cookie.js +30 -0
  14. package/animals/crab.js +32 -0
  15. package/animals/crocodile.js +32 -0
  16. package/animals/deer.js +35 -0
  17. package/animals/dino.js +37 -0
  18. package/animals/dog.js +44 -0
  19. package/animals/dragon.js +44 -0
  20. package/animals/duck.js +45 -0
  21. package/animals/eagle.js +37 -0
  22. package/animals/elephant.js +33 -0
  23. package/animals/firefly.js +37 -0
  24. package/animals/fish.js +32 -0
  25. package/animals/flamingo.js +36 -0
  26. package/animals/fox.js +43 -0
  27. package/animals/frog.js +46 -0
  28. package/animals/ghost.js +32 -0
  29. package/animals/giraffe.js +37 -0
  30. package/animals/gorilla.js +37 -0
  31. package/animals/hamster.js +35 -0
  32. package/animals/hedgehog.js +32 -0
  33. package/animals/hippo.js +34 -0
  34. package/animals/index.js +87 -0
  35. package/animals/jellyfish.js +34 -0
  36. package/animals/koala.js +34 -0
  37. package/animals/ladybug.js +34 -0
  38. package/animals/lion.js +36 -0
  39. package/animals/mantisshrimp.js +49 -0
  40. package/animals/monkey.js +35 -0
  41. package/animals/moth.js +41 -0
  42. package/animals/mouse.js +33 -0
  43. package/animals/narwhal.js +38 -0
  44. package/animals/octopus.js +31 -0
  45. package/animals/otter.js +43 -0
  46. package/animals/owl.js +38 -0
  47. package/animals/panda.js +42 -0
  48. package/animals/parrot.js +39 -0
  49. package/animals/peacock.js +42 -0
  50. package/animals/penguin.js +47 -0
  51. package/animals/penguin2.js +38 -0
  52. package/animals/pig.js +33 -0
  53. package/animals/raccoon.js +35 -0
  54. package/animals/redpanda.js +39 -0
  55. package/animals/robot.js +37 -0
  56. package/animals/scorpion.js +33 -0
  57. package/animals/seahorse.js +39 -0
  58. package/animals/shark.js +33 -0
  59. package/animals/shit.js +33 -0
  60. package/animals/sloth.js +34 -0
  61. package/animals/snail.js +34 -0
  62. package/animals/snake.js +36 -0
  63. package/animals/spider.js +33 -0
  64. package/animals/squid.js +33 -0
  65. package/animals/starfish.js +32 -0
  66. package/animals/stingray.js +41 -0
  67. package/animals/swan.js +34 -0
  68. package/animals/toucan.js +41 -0
  69. package/animals/turtle.js +35 -0
  70. package/animals/unicorn.js +40 -0
  71. package/animals/walrus.js +35 -0
  72. package/animals/whale.js +42 -0
  73. package/animals/wolf.js +36 -0
  74. package/animals/yak.js +36 -0
  75. package/animals/zebra.js +34 -0
  76. package/color.js +9 -0
  77. package/completions/_meeb +41 -0
  78. package/completions/meeb.bash +16 -0
  79. package/emoji.js +30 -0
  80. package/fortune.js +41 -0
  81. package/gif.js +435 -0
  82. package/index.js +915 -0
  83. package/info.js +80 -0
  84. package/package.json +39 -0
  85. package/png.js +88 -0
  86. package/svg.js +43 -0
  87. package/theme.js +80 -0
  88. package/tui.js +76 -0
  89. package/util.js +313 -0
package/gif.js ADDED
@@ -0,0 +1,435 @@
1
+ const fs = require('fs')
2
+ const { stripAnsi } = require('./util')
3
+
4
+ // Parse ANSI art into a grid of {char, fg, bg} cells
5
+ function parseAnsi(art) {
6
+ const lines = art.split('\n')
7
+ const grid = []
8
+ for (const line of lines) {
9
+ const row = []
10
+ let fg = [200, 200, 200]
11
+ let bg = [0, 0, 0]
12
+ let i = 0
13
+ while (i < line.length) {
14
+ if (line[i] === '\x1b' && line[i + 1] === '[') {
15
+ // parse escape sequence
16
+ let j = i + 2
17
+ while (j < line.length && line[j] !== 'm') j++
18
+ const codes = line.slice(i + 2, j).split(';').map(Number)
19
+ let k = 0
20
+ while (k < codes.length) {
21
+ if (codes[k] === 0) {
22
+ fg = [200, 200, 200]
23
+ bg = [0, 0, 0]
24
+ } else if (codes[k] === 38 && codes[k + 1] === 2) {
25
+ fg = [codes[k + 2], codes[k + 3], codes[k + 4]]
26
+ k += 4
27
+ } else if (codes[k] === 48 && codes[k + 1] === 2) {
28
+ bg = [codes[k + 2], codes[k + 3], codes[k + 4]]
29
+ k += 4
30
+ }
31
+ k++
32
+ }
33
+ i = j + 1
34
+ } else {
35
+ row.push({ char: line[i], fg: [...fg], bg: [...bg] })
36
+ i++
37
+ }
38
+ }
39
+ grid.push(row)
40
+ }
41
+ return grid
42
+ }
43
+
44
+ // Block characters and how they fill a 2x2 sub-pixel cell (top-left, top-right, bottom-left, bottom-right)
45
+ // Each char maps to which sub-pixels are "foreground" vs "background"
46
+ // We render each character cell as cellW x cellH pixels
47
+ const CELL_W = 8
48
+ const CELL_H = 14
49
+
50
+ function renderCell(char, fg, bg) {
51
+ // Returns a cellW x cellH pixel grid
52
+ const pixels = []
53
+ for (let y = 0; y < CELL_H; y++) pixels.push(new Array(CELL_W))
54
+
55
+ const topHalf = y => y < CELL_H / 2
56
+ const botHalf = y => y >= CELL_H / 2
57
+ const leftHalf = x => x < CELL_W / 2
58
+ const rightHalf = x => x >= CELL_W / 2
59
+
60
+ for (let y = 0; y < CELL_H; y++) {
61
+ for (let x = 0; x < CELL_W; x++) {
62
+ let color = bg
63
+ switch (char) {
64
+ case '█': case '▓': color = fg; break
65
+ case '░': color = (x + y) % 3 === 0 ? fg : bg; break
66
+ case '▄': color = botHalf(y) ? fg : bg; break
67
+ case '▀': color = topHalf(y) ? fg : bg; break
68
+ case '▐': color = rightHalf(x) ? fg : bg; break
69
+ case '▌': color = leftHalf(x) ? fg : bg; break
70
+ case '▛': color = (topHalf(y) || leftHalf(x)) ? fg : bg; break
71
+ case '▜': color = (topHalf(y) || rightHalf(x)) ? fg : bg; break
72
+ case '▙': color = (botHalf(y) || leftHalf(x)) ? fg : bg; break
73
+ case '▟': color = (botHalf(y) || rightHalf(x)) ? fg : bg; break
74
+ case '▗': color = (botHalf(y) && rightHalf(x)) ? fg : bg; break
75
+ case '▖': color = (botHalf(y) && leftHalf(x)) ? fg : bg; break
76
+ case '▝': color = (topHalf(y) && rightHalf(x)) ? fg : bg; break
77
+ case '▘': color = (topHalf(y) && leftHalf(x)) ? fg : bg; break
78
+ case '●': case '◉': {
79
+ const cx = CELL_W / 2, cy = CELL_H / 2
80
+ const dx = x - cx + 0.5, dy = (y - cy + 0.5) * (CELL_W / CELL_H)
81
+ color = Math.sqrt(dx * dx + dy * dy) < CELL_W / 2.5 ? fg : bg
82
+ break
83
+ }
84
+ case '▼': case '▽': {
85
+ const progress = y / CELL_H
86
+ const halfW = (1 - progress) * CELL_W / 2
87
+ color = (x >= CELL_W / 2 - halfW && x < CELL_W / 2 + halfW) ? fg : bg
88
+ break
89
+ }
90
+ case '✦': case '·': case '★': {
91
+ const cx = CELL_W / 2, cy = CELL_H / 2
92
+ const dx = Math.abs(x - cx + 0.5), dy = Math.abs((y - cy + 0.5) * (CELL_W / CELL_H))
93
+ color = (dx + dy < CELL_W / 2.5) ? fg : bg
94
+ break
95
+ }
96
+ default:
97
+ // most text chars: just fill with fg if not space
98
+ if (char !== ' ' && char !== '') color = fg
99
+ break
100
+ }
101
+ pixels[y][x] = color
102
+ }
103
+ }
104
+ return pixels
105
+ }
106
+
107
+ function artToPixels(art, bgColor = [25, 25, 30]) {
108
+ const grid = parseAnsi(art)
109
+ if (grid.length === 0) return { pixels: [], w: 0, h: 0 }
110
+
111
+ const maxCols = Math.max(...grid.map(r => r.length))
112
+ const padding = 2 // cells of padding
113
+ const totalCols = maxCols + padding * 2
114
+ const totalRows = grid.length + padding * 2
115
+
116
+ const w = totalCols * CELL_W
117
+ const h = totalRows * CELL_H
118
+ const pixels = []
119
+ for (let y = 0; y < h; y++) pixels.push(new Array(w))
120
+
121
+ // fill with background
122
+ for (let y = 0; y < h; y++)
123
+ for (let x = 0; x < w; x++)
124
+ pixels[y][x] = bgColor
125
+
126
+ for (let row = 0; row < grid.length; row++) {
127
+ for (let col = 0; col < grid[row].length; col++) {
128
+ const cell = grid[row][col]
129
+ const cellPixels = renderCell(cell.char, cell.fg, cell.bg)
130
+ const ox = (col + padding) * CELL_W
131
+ const oy = (row + padding) * CELL_H
132
+ for (let cy = 0; cy < CELL_H; cy++) {
133
+ for (let cx = 0; cx < CELL_W; cx++) {
134
+ pixels[oy + cy][ox + cx] = cellPixels[cy][cx]
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return { pixels, w, h }
140
+ }
141
+
142
+ // --- GIF89a encoder ---
143
+
144
+ // Build a color table from pixel data (max 256 colors via median cut)
145
+ function buildPalette(pixels, maxColors = 256) {
146
+ // Collect unique colors (quantized to reduce)
147
+ const colorCounts = new Map()
148
+ for (const row of pixels) {
149
+ for (const [r, g, b] of row) {
150
+ // quantize to 6-bit per channel for grouping
151
+ const qr = r >> 2, qg = g >> 2, qb = b >> 2
152
+ const key = (qr << 12) | (qg << 6) | qb
153
+ colorCounts.set(key, (colorCounts.get(key) || 0) + 1)
154
+ }
155
+ }
156
+
157
+ // Get top N colors by frequency
158
+ const sorted = [...colorCounts.entries()].sort((a, b) => b[1] - a[1])
159
+ const palette = []
160
+ for (let i = 0; i < Math.min(maxColors, sorted.length); i++) {
161
+ const key = sorted[i][0]
162
+ const r = ((key >> 12) & 0x3f) << 2
163
+ const g = ((key >> 6) & 0x3f) << 2
164
+ const b = (key & 0x3f) << 2
165
+ palette.push([r, g, b])
166
+ }
167
+
168
+ // pad to power of 2
169
+ while (palette.length < 2) palette.push([0, 0, 0])
170
+ let bits = 1
171
+ while ((1 << bits) < palette.length) bits++
172
+ while (palette.length < (1 << bits)) palette.push([0, 0, 0])
173
+
174
+ return { palette, bits }
175
+ }
176
+
177
+ function findClosest(palette, r, g, b) {
178
+ let best = 0, bestD = Infinity
179
+ const qr = r >> 2 << 2, qg = g >> 2 << 2, qb = b >> 2 << 2
180
+ for (let i = 0; i < palette.length; i++) {
181
+ const dr = palette[i][0] - qr
182
+ const dg = palette[i][1] - qg
183
+ const db = palette[i][2] - qb
184
+ const d = dr * dr + dg * dg + db * db
185
+ if (d === 0) return i
186
+ if (d < bestD) { bestD = d; best = i }
187
+ }
188
+ return best
189
+ }
190
+
191
+ function indexPixels(pixels, palette) {
192
+ const h = pixels.length, w = pixels[0].length
193
+ const indices = new Uint8Array(w * h)
194
+ // build lookup cache
195
+ const cache = new Map()
196
+ for (let y = 0; y < h; y++) {
197
+ for (let x = 0; x < w; x++) {
198
+ const [r, g, b] = pixels[y][x]
199
+ const key = (r << 16) | (g << 8) | b
200
+ if (cache.has(key)) {
201
+ indices[y * w + x] = cache.get(key)
202
+ } else {
203
+ const idx = findClosest(palette, r, g, b)
204
+ cache.set(key, idx)
205
+ indices[y * w + x] = idx
206
+ }
207
+ }
208
+ }
209
+ return indices
210
+ }
211
+
212
+ // LZW compression for GIF
213
+ function lzwEncode(indices, minCodeSize) {
214
+ const clearCode = 1 << minCodeSize
215
+ const eoiCode = clearCode + 1
216
+
217
+ const output = []
218
+ let codeSize = minCodeSize + 1
219
+ let nextCode = eoiCode + 1
220
+ const maxTableSize = 4096
221
+
222
+ // init table
223
+ let table = new Map()
224
+ for (let i = 0; i < clearCode; i++) table.set(String(i), i)
225
+
226
+ let buffer = 0
227
+ let bitsInBuf = 0
228
+
229
+ function emit(code) {
230
+ buffer |= (code << bitsInBuf)
231
+ bitsInBuf += codeSize
232
+ while (bitsInBuf >= 8) {
233
+ output.push(buffer & 0xff)
234
+ buffer >>= 8
235
+ bitsInBuf -= 8
236
+ }
237
+ }
238
+
239
+ emit(clearCode)
240
+
241
+ let prev = String(indices[0])
242
+ for (let i = 1; i < indices.length; i++) {
243
+ const curr = String(indices[i])
244
+ const combined = prev + ',' + curr
245
+ if (table.has(combined)) {
246
+ prev = combined
247
+ } else {
248
+ emit(table.get(prev))
249
+ if (nextCode < maxTableSize) {
250
+ table.set(combined, nextCode++)
251
+ if (nextCode > (1 << codeSize) && codeSize < 12) codeSize++
252
+ } else {
253
+ // reset
254
+ emit(clearCode)
255
+ table = new Map()
256
+ for (let j = 0; j < clearCode; j++) table.set(String(j), j)
257
+ nextCode = eoiCode + 1
258
+ codeSize = minCodeSize + 1
259
+ }
260
+ prev = curr
261
+ }
262
+ }
263
+ emit(table.get(prev))
264
+ emit(eoiCode)
265
+
266
+ // flush remaining bits
267
+ if (bitsInBuf > 0) output.push(buffer & 0xff)
268
+
269
+ return Buffer.from(output)
270
+ }
271
+
272
+ function subBlock(data) {
273
+ // split into 255-byte sub-blocks
274
+ const chunks = []
275
+ let i = 0
276
+ while (i < data.length) {
277
+ const size = Math.min(255, data.length - i)
278
+ chunks.push(size)
279
+ for (let j = 0; j < size; j++) chunks.push(data[i + j])
280
+ i += size
281
+ }
282
+ chunks.push(0) // block terminator
283
+ return Buffer.from(chunks)
284
+ }
285
+
286
+ function encodeGif(frames, delays, w, h) {
287
+ const bufs = []
288
+
289
+ // use first frame for palette
290
+ const { palette, bits } = buildPalette(frames[0])
291
+
292
+ // Header
293
+ bufs.push(Buffer.from('GIF89a'))
294
+
295
+ // Logical Screen Descriptor
296
+ const lsd = Buffer.alloc(7)
297
+ lsd.writeUInt16LE(w, 0)
298
+ lsd.writeUInt16LE(h, 2)
299
+ lsd.writeUInt8(0x80 | ((bits - 1) << 4) | (bits - 1), 4) // GCT flag + color resolution + GCT size
300
+ lsd.writeUInt8(0, 5) // bg color index
301
+ lsd.writeUInt8(0, 6) // pixel aspect ratio
302
+ bufs.push(lsd)
303
+
304
+ // Global Color Table
305
+ const gct = Buffer.alloc(3 * (1 << bits))
306
+ for (let i = 0; i < palette.length; i++) {
307
+ gct[i * 3] = palette[i][0]
308
+ gct[i * 3 + 1] = palette[i][1]
309
+ gct[i * 3 + 2] = palette[i][2]
310
+ }
311
+ bufs.push(gct)
312
+
313
+ // Netscape loop extension
314
+ bufs.push(Buffer.from([
315
+ 0x21, 0xff, 0x0b,
316
+ ...Buffer.from('NETSCAPE2.0'),
317
+ 0x03, 0x01, 0x00, 0x00, // loop forever
318
+ 0x00
319
+ ]))
320
+
321
+ for (let f = 0; f < frames.length; f++) {
322
+ // Graphic Control Extension
323
+ const gce = Buffer.alloc(8)
324
+ gce[0] = 0x21 // extension
325
+ gce[1] = 0xf9 // graphic control
326
+ gce[2] = 0x04 // block size
327
+ gce[3] = 0x00 // no transparency, no disposal
328
+ gce.writeUInt16LE(delays[f], 4) // delay in 1/100s
329
+ gce[6] = 0x00 // transparent color index
330
+ gce[7] = 0x00 // block terminator
331
+ bufs.push(gce)
332
+
333
+ // Image Descriptor
334
+ const imgDesc = Buffer.alloc(10)
335
+ imgDesc[0] = 0x2c // image separator
336
+ imgDesc.writeUInt16LE(0, 1) // left
337
+ imgDesc.writeUInt16LE(0, 3) // top
338
+ imgDesc.writeUInt16LE(w, 5) // width
339
+ imgDesc.writeUInt16LE(h, 7) // height
340
+ imgDesc[9] = 0x00 // no local color table
341
+ bufs.push(imgDesc)
342
+
343
+ // Image Data
344
+ const indices = indexPixels(frames[f], palette)
345
+ const minCodeSize = Math.max(2, bits)
346
+ bufs.push(Buffer.from([minCodeSize]))
347
+ const compressed = lzwEncode(indices, minCodeSize)
348
+ bufs.push(subBlock(compressed))
349
+ }
350
+
351
+ // Trailer
352
+ bufs.push(Buffer.from([0x3b]))
353
+
354
+ return Buffer.concat(bufs)
355
+ }
356
+
357
+ function scalePixels(pixels, w, h, scaleX, scaleY) {
358
+ if (scaleY === undefined) scaleY = scaleX
359
+ const nw = Math.round(w * scaleX), nh = Math.round(h * scaleY)
360
+ const out = []
361
+ for (let y = 0; y < nh; y++) {
362
+ const row = new Array(nw)
363
+ const sy = Math.min(Math.floor(y / scaleY), h - 1)
364
+ for (let x = 0; x < nw; x++) {
365
+ row[x] = pixels[sy][Math.min(Math.floor(x / scaleX), w - 1)]
366
+ }
367
+ out.push(row)
368
+ }
369
+ return { pixels: out, w: nw, h: nh }
370
+ }
371
+
372
+ function applyWhiteBg(pixels, w, h, bgColor) {
373
+ if (bgColor[0] !== 255 || bgColor[1] !== 255 || bgColor[2] !== 255) return
374
+ for (let y = 0; y < h; y++)
375
+ for (let x = 0; x < w; x++) {
376
+ const [r, g, b] = pixels[y][x]
377
+ if (r === 0 && g === 0 && b === 0) pixels[y][x] = bgColor
378
+ }
379
+ }
380
+
381
+ function correctAspect(pixels, w, h) {
382
+ return scalePixels(pixels, w, h, 1, CELL_W / CELL_H)
383
+ }
384
+
385
+ function generateGif(art, outPath, bgColor) {
386
+ const bg = bgColor || [25, 25, 30]
387
+ let { pixels, w, h } = artToPixels(art, bg)
388
+ applyWhiteBg(pixels, w, h, bg)
389
+ const corrected = correctAspect(pixels, w, h)
390
+ if (corrected.w === 0 || corrected.h === 0) {
391
+ console.error(' empty art, cannot generate gif')
392
+ process.exit(1)
393
+ }
394
+
395
+ const gif = encodeGif([corrected.pixels], [100], corrected.w, corrected.h)
396
+ fs.writeFileSync(outPath, gif)
397
+ return outPath
398
+ }
399
+
400
+ function generateAnimatedGif(arts, outPath, delay = 80, bgColor) {
401
+ const bg = bgColor || [25, 25, 30]
402
+ const frames = []
403
+ let maxW = 0, maxH = 0
404
+
405
+ const parsed = arts.map(art => {
406
+ let { pixels, w, h } = artToPixels(art, bg)
407
+ applyWhiteBg(pixels, w, h, bg)
408
+ const corrected = correctAspect(pixels, w, h)
409
+ return corrected
410
+ })
411
+ for (const { w, h } of parsed) {
412
+ maxW = Math.max(maxW, w)
413
+ maxH = Math.max(maxH, h)
414
+ }
415
+
416
+ for (const { pixels, w, h } of parsed) {
417
+ const frame = []
418
+ for (let y = 0; y < maxH; y++) {
419
+ const row = []
420
+ for (let x = 0; x < maxW; x++) {
421
+ if (y < h && x < w) row.push(pixels[y][x])
422
+ else row.push(bg)
423
+ }
424
+ frame.push(row)
425
+ }
426
+ frames.push(frame)
427
+ }
428
+
429
+ const delays = frames.map(() => delay)
430
+ const gif = encodeGif(frames, delays, maxW, maxH)
431
+ fs.writeFileSync(outPath, gif)
432
+ return outPath
433
+ }
434
+
435
+ module.exports = { generateGif, generateAnimatedGif, artToPixels, parseAnsi, renderCell, scalePixels, applyWhiteBg, correctAspect, CELL_W, CELL_H }