unicode-animations 0.2.1 → 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/README.md CHANGED
@@ -4,7 +4,7 @@ Unicode spinner animations as raw frame data — no dependencies, works everywhe
4
4
 
5
5
  ## Demo
6
6
 
7
- See all 22 spinners animating live:
7
+ See all 18 spinners animating live:
8
8
 
9
9
  ```bash
10
10
  npx unicode-animations --web # open browser demo
@@ -171,15 +171,6 @@ el.textContent = '✔ Synced';
171
171
  | `fillsweep` | 11 | 100ms |
172
172
  | `diagswipe` | 16 | 60ms |
173
173
 
174
- ### Non-braille classics
175
-
176
- | Name | Preview | Interval |
177
- |------|---------|----------|
178
- | `arc` | `◜ ◠ ◝ ◞ ◡ ◟` | 100ms |
179
- | `halfmoon` | `◐ ◓ ◑ ◒` | 180ms |
180
- | `line` | `\| / — \` | 100ms |
181
- | `blocks` | `▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▇ ▆ ▅ ▄ ▃ ▂` | 100ms |
182
-
183
174
  ## Custom spinners
184
175
 
185
176
  Create your own braille spinners using the grid utilities:
@@ -218,7 +209,7 @@ interface Spinner {
218
209
  | `gridToBraille(grid)` | `(boolean[][]) => string` |
219
210
  | `makeGrid(rows, cols)` | `(number, number) => boolean[][]` |
220
211
  | `Spinner` | TypeScript interface |
221
- | `BrailleSpinnerName` | Union type of all 22 spinner names |
212
+ | `BrailleSpinnerName` | Union type of all 18 spinner names |
222
213
 
223
214
  ### Exports from `'unicode-animations/braille'`
224
215
 
package/dist/braille.cjs CHANGED
@@ -56,6 +56,7 @@ function gridToBraille(grid) {
56
56
  return result;
57
57
  }
58
58
  function makeGrid(rows, cols) {
59
+ if (rows <= 0 || cols <= 0) return [];
59
60
  return Array.from({ length: rows }, () => Array(cols).fill(false));
60
61
  }
61
62
  function genScan() {
@@ -377,24 +378,7 @@ var spinners = {
377
378
  checkerboard: { frames: genCheckerboard(), interval: 250 },
378
379
  helix: { frames: genHelix(), interval: 80 },
379
380
  fillsweep: { frames: genFillSweep(), interval: 100 },
380
- diagswipe: { frames: genDiagonalSwipe(), interval: 60 },
381
- // === Non-braille classics ===
382
- arc: {
383
- frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"],
384
- interval: 100
385
- },
386
- halfmoon: {
387
- frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"],
388
- interval: 180
389
- },
390
- line: {
391
- frames: ["|", "/", "\u2014", "\\"],
392
- interval: 100
393
- },
394
- blocks: {
395
- frames: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588", "\u2587", "\u2586", "\u2585", "\u2584", "\u2583", "\u2582"],
396
- interval: 100
397
- }
381
+ diagswipe: { frames: genDiagonalSwipe(), interval: 60 }
398
382
  };
399
383
  var braille_default = spinners;
400
384
  // Annotate the CommonJS export names for ESM import in node:
@@ -9,7 +9,7 @@ interface Spinner {
9
9
  readonly frames: readonly string[];
10
10
  readonly interval: number;
11
11
  }
12
- type BrailleSpinnerName = 'braille' | 'braillewave' | 'dna' | 'scan' | 'rain' | 'scanline' | 'pulse' | 'snake' | 'sparkle' | 'cascade' | 'columns' | 'orbit' | 'breathe' | 'waverows' | 'checkerboard' | 'helix' | 'fillsweep' | 'diagswipe' | 'arc' | 'halfmoon' | 'line' | 'blocks';
12
+ type BrailleSpinnerName = 'braille' | 'braillewave' | 'dna' | 'scan' | 'rain' | 'scanline' | 'pulse' | 'snake' | 'sparkle' | 'cascade' | 'columns' | 'orbit' | 'breathe' | 'waverows' | 'checkerboard' | 'helix' | 'fillsweep' | 'diagswipe';
13
13
  /**
14
14
  * Convert a 2D boolean grid into a braille string.
15
15
  * grid[row][col] = true means dot is raised.
package/dist/braille.d.ts CHANGED
@@ -9,7 +9,7 @@ interface Spinner {
9
9
  readonly frames: readonly string[];
10
10
  readonly interval: number;
11
11
  }
12
- type BrailleSpinnerName = 'braille' | 'braillewave' | 'dna' | 'scan' | 'rain' | 'scanline' | 'pulse' | 'snake' | 'sparkle' | 'cascade' | 'columns' | 'orbit' | 'breathe' | 'waverows' | 'checkerboard' | 'helix' | 'fillsweep' | 'diagswipe' | 'arc' | 'halfmoon' | 'line' | 'blocks';
12
+ type BrailleSpinnerName = 'braille' | 'braillewave' | 'dna' | 'scan' | 'rain' | 'scanline' | 'pulse' | 'snake' | 'sparkle' | 'cascade' | 'columns' | 'orbit' | 'breathe' | 'waverows' | 'checkerboard' | 'helix' | 'fillsweep' | 'diagswipe';
13
13
  /**
14
14
  * Convert a 2D boolean grid into a braille string.
15
15
  * grid[row][col] = true means dot is raised.
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var UnicodeAnimations = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/braille.ts
22
+ var braille_exports = {};
23
+ __export(braille_exports, {
24
+ default: () => braille_default,
25
+ gridToBraille: () => gridToBraille,
26
+ makeGrid: () => makeGrid,
27
+ spinners: () => spinners
28
+ });
29
+ var BRAILLE_DOT_MAP = [
30
+ [1, 8],
31
+ // row 0
32
+ [2, 16],
33
+ // row 1
34
+ [4, 32],
35
+ // row 2
36
+ [64, 128]
37
+ // row 3
38
+ ];
39
+ function gridToBraille(grid) {
40
+ const rows = grid.length;
41
+ const cols = grid[0] ? grid[0].length : 0;
42
+ const charCount = Math.ceil(cols / 2);
43
+ let result = "";
44
+ for (let c = 0; c < charCount; c++) {
45
+ let code = 10240;
46
+ for (let r = 0; r < 4 && r < rows; r++) {
47
+ for (let d = 0; d < 2; d++) {
48
+ const col = c * 2 + d;
49
+ if (col < cols && grid[r] && grid[r][col]) {
50
+ code |= BRAILLE_DOT_MAP[r][d];
51
+ }
52
+ }
53
+ }
54
+ result += String.fromCodePoint(code);
55
+ }
56
+ return result;
57
+ }
58
+ function makeGrid(rows, cols) {
59
+ if (rows <= 0 || cols <= 0) return [];
60
+ return Array.from({ length: rows }, () => Array(cols).fill(false));
61
+ }
62
+ function genScan() {
63
+ const W = 8, H = 4, frames = [];
64
+ for (let pos = -1; pos < W + 1; pos++) {
65
+ const g = makeGrid(H, W);
66
+ for (let r = 0; r < H; r++) {
67
+ for (let c = 0; c < W; c++) {
68
+ if (c === pos || c === pos - 1) g[r][c] = true;
69
+ }
70
+ }
71
+ frames.push(gridToBraille(g));
72
+ }
73
+ return frames;
74
+ }
75
+ function genRain() {
76
+ const W = 8, H = 4, totalFrames = 12, frames = [];
77
+ const offsets = [0, 3, 1, 5, 2, 7, 4, 6];
78
+ for (let f = 0; f < totalFrames; f++) {
79
+ const g = makeGrid(H, W);
80
+ for (let c = 0; c < W; c++) {
81
+ const row = (f + offsets[c]) % (H + 2);
82
+ if (row < H) g[row][c] = true;
83
+ }
84
+ frames.push(gridToBraille(g));
85
+ }
86
+ return frames;
87
+ }
88
+ function genScanLine() {
89
+ const W = 6, H = 4, frames = [];
90
+ const positions = [0, 1, 2, 3, 2, 1];
91
+ for (const row of positions) {
92
+ const g = makeGrid(H, W);
93
+ for (let c = 0; c < W; c++) {
94
+ g[row][c] = true;
95
+ if (row > 0) g[row - 1][c] = c % 2 === 0;
96
+ }
97
+ frames.push(gridToBraille(g));
98
+ }
99
+ return frames;
100
+ }
101
+ function genPulse() {
102
+ const W = 6, H = 4, frames = [];
103
+ const cx = W / 2 - 0.5, cy = H / 2 - 0.5;
104
+ const radii = [0.5, 1.2, 2, 3, 3.5];
105
+ for (const r of radii) {
106
+ const g = makeGrid(H, W);
107
+ for (let row = 0; row < H; row++) {
108
+ for (let col = 0; col < W; col++) {
109
+ const dist = Math.sqrt((col - cx) ** 2 + (row - cy) ** 2);
110
+ if (Math.abs(dist - r) < 0.9) g[row][col] = true;
111
+ }
112
+ }
113
+ frames.push(gridToBraille(g));
114
+ }
115
+ return frames;
116
+ }
117
+ function genSnake() {
118
+ const W = 4, H = 4;
119
+ const path = [];
120
+ for (let r = 0; r < H; r++) {
121
+ if (r % 2 === 0) {
122
+ for (let c = 0; c < W; c++) path.push([r, c]);
123
+ } else {
124
+ for (let c = W - 1; c >= 0; c--) path.push([r, c]);
125
+ }
126
+ }
127
+ const frames = [];
128
+ for (let i = 0; i < path.length; i++) {
129
+ const g = makeGrid(H, W);
130
+ for (let t = 0; t < 4; t++) {
131
+ const idx = (i - t + path.length) % path.length;
132
+ g[path[idx][0]][path[idx][1]] = true;
133
+ }
134
+ frames.push(gridToBraille(g));
135
+ }
136
+ return frames;
137
+ }
138
+ function genSparkle() {
139
+ const patterns = [
140
+ [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0],
141
+ [0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0],
142
+ [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1],
143
+ [1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0],
144
+ [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
145
+ [0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0]
146
+ ];
147
+ const W = 8, H = 4, frames = [];
148
+ for (const pat of patterns) {
149
+ const g = makeGrid(H, W);
150
+ for (let r = 0; r < H; r++) {
151
+ for (let c = 0; c < W; c++) {
152
+ g[r][c] = !!pat[r * W + c];
153
+ }
154
+ }
155
+ frames.push(gridToBraille(g));
156
+ }
157
+ return frames;
158
+ }
159
+ function genCascade() {
160
+ const W = 8, H = 4, frames = [];
161
+ for (let offset = -2; offset < W + H; offset++) {
162
+ const g = makeGrid(H, W);
163
+ for (let r = 0; r < H; r++) {
164
+ for (let c = 0; c < W; c++) {
165
+ const diag = c + r;
166
+ if (diag === offset || diag === offset - 1) g[r][c] = true;
167
+ }
168
+ }
169
+ frames.push(gridToBraille(g));
170
+ }
171
+ return frames;
172
+ }
173
+ function genColumns() {
174
+ const W = 6, H = 4, frames = [];
175
+ for (let col = 0; col < W; col++) {
176
+ for (let fillTo = H - 1; fillTo >= 0; fillTo--) {
177
+ const g = makeGrid(H, W);
178
+ for (let pc = 0; pc < col; pc++) {
179
+ for (let r = 0; r < H; r++) g[r][pc] = true;
180
+ }
181
+ for (let r = fillTo; r < H; r++) g[r][col] = true;
182
+ frames.push(gridToBraille(g));
183
+ }
184
+ }
185
+ const full = makeGrid(H, W);
186
+ for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) full[r][c] = true;
187
+ frames.push(gridToBraille(full));
188
+ frames.push(gridToBraille(makeGrid(H, W)));
189
+ return frames;
190
+ }
191
+ function genOrbit() {
192
+ const W = 2, H = 4;
193
+ const path = [
194
+ [0, 0],
195
+ [0, 1],
196
+ [1, 1],
197
+ [2, 1],
198
+ [3, 1],
199
+ [3, 0],
200
+ [2, 0],
201
+ [1, 0]
202
+ ];
203
+ const frames = [];
204
+ for (let i = 0; i < path.length; i++) {
205
+ const g = makeGrid(H, W);
206
+ g[path[i][0]][path[i][1]] = true;
207
+ const t1 = (i - 1 + path.length) % path.length;
208
+ g[path[t1][0]][path[t1][1]] = true;
209
+ frames.push(gridToBraille(g));
210
+ }
211
+ return frames;
212
+ }
213
+ function genBreathe() {
214
+ const stages = [
215
+ [],
216
+ [[1, 0]],
217
+ [[0, 1], [2, 0]],
218
+ [[0, 0], [1, 1], [3, 0]],
219
+ [[0, 0], [1, 1], [2, 0], [3, 1]],
220
+ [[0, 0], [0, 1], [1, 1], [2, 0], [3, 1]],
221
+ [[0, 0], [0, 1], [1, 0], [2, 1], [3, 0], [3, 1]],
222
+ [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [3, 0], [3, 1]],
223
+ [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1], [3, 0], [3, 1]]
224
+ ];
225
+ const frames = [];
226
+ const sequence = [...stages, ...stages.slice().reverse().slice(1)];
227
+ for (const dots of sequence) {
228
+ const g = makeGrid(4, 2);
229
+ for (const [r, c] of dots) g[r][c] = true;
230
+ frames.push(gridToBraille(g));
231
+ }
232
+ return frames;
233
+ }
234
+ function genWaveRows() {
235
+ const W = 8, H = 4, totalFrames = 16, frames = [];
236
+ for (let f = 0; f < totalFrames; f++) {
237
+ const g = makeGrid(H, W);
238
+ for (let c = 0; c < W; c++) {
239
+ const phase = f - c * 0.5;
240
+ const row = Math.round((Math.sin(phase * 0.8) + 1) / 2 * (H - 1));
241
+ g[row][c] = true;
242
+ if (row > 0) g[row - 1][c] = (f + c) % 3 === 0;
243
+ }
244
+ frames.push(gridToBraille(g));
245
+ }
246
+ return frames;
247
+ }
248
+ function genCheckerboard() {
249
+ const W = 6, H = 4, frames = [];
250
+ for (let phase = 0; phase < 4; phase++) {
251
+ const g = makeGrid(H, W);
252
+ for (let r = 0; r < H; r++) {
253
+ for (let c = 0; c < W; c++) {
254
+ if (phase < 2) {
255
+ g[r][c] = (r + c + phase) % 2 === 0;
256
+ } else {
257
+ g[r][c] = (r + c + phase) % 3 === 0;
258
+ }
259
+ }
260
+ }
261
+ frames.push(gridToBraille(g));
262
+ }
263
+ return frames;
264
+ }
265
+ function genHelix() {
266
+ const W = 8, H = 4, totalFrames = 16, frames = [];
267
+ for (let f = 0; f < totalFrames; f++) {
268
+ const g = makeGrid(H, W);
269
+ for (let c = 0; c < W; c++) {
270
+ const phase = (f + c) * (Math.PI / 4);
271
+ const y1 = Math.round((Math.sin(phase) + 1) / 2 * (H - 1));
272
+ const y2 = Math.round((Math.sin(phase + Math.PI) + 1) / 2 * (H - 1));
273
+ g[y1][c] = true;
274
+ g[y2][c] = true;
275
+ }
276
+ frames.push(gridToBraille(g));
277
+ }
278
+ return frames;
279
+ }
280
+ function genFillSweep() {
281
+ const W = 4, H = 4, frames = [];
282
+ for (let row = H - 1; row >= 0; row--) {
283
+ const g = makeGrid(H, W);
284
+ for (let r = row; r < H; r++) {
285
+ for (let c = 0; c < W; c++) g[r][c] = true;
286
+ }
287
+ frames.push(gridToBraille(g));
288
+ }
289
+ const full = makeGrid(H, W);
290
+ for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) full[r][c] = true;
291
+ frames.push(gridToBraille(full));
292
+ frames.push(gridToBraille(full));
293
+ for (let row = 0; row < H; row++) {
294
+ const g = makeGrid(H, W);
295
+ for (let r = row + 1; r < H; r++) {
296
+ for (let c = 0; c < W; c++) g[r][c] = true;
297
+ }
298
+ frames.push(gridToBraille(g));
299
+ }
300
+ frames.push(gridToBraille(makeGrid(H, W)));
301
+ return frames;
302
+ }
303
+ function genDiagonalSwipe() {
304
+ const W = 4, H = 4, frames = [];
305
+ const maxDiag = W + H - 2;
306
+ for (let d = 0; d <= maxDiag; d++) {
307
+ const g = makeGrid(H, W);
308
+ for (let r = 0; r < H; r++) {
309
+ for (let c = 0; c < W; c++) {
310
+ if (r + c <= d) g[r][c] = true;
311
+ }
312
+ }
313
+ frames.push(gridToBraille(g));
314
+ }
315
+ const full = makeGrid(H, W);
316
+ for (let r = 0; r < H; r++) for (let c = 0; c < W; c++) full[r][c] = true;
317
+ frames.push(gridToBraille(full));
318
+ for (let d = 0; d <= maxDiag; d++) {
319
+ const g = makeGrid(H, W);
320
+ for (let r = 0; r < H; r++) {
321
+ for (let c = 0; c < W; c++) {
322
+ if (r + c > d) g[r][c] = true;
323
+ }
324
+ }
325
+ frames.push(gridToBraille(g));
326
+ }
327
+ frames.push(gridToBraille(makeGrid(H, W)));
328
+ return frames;
329
+ }
330
+ var spinners = {
331
+ // === Classic braille single-char ===
332
+ braille: {
333
+ frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
334
+ interval: 80
335
+ },
336
+ braillewave: {
337
+ frames: [
338
+ "\u2801\u2802\u2804\u2840",
339
+ "\u2802\u2804\u2840\u2880",
340
+ "\u2804\u2840\u2880\u2820",
341
+ "\u2840\u2880\u2820\u2810",
342
+ "\u2880\u2820\u2810\u2808",
343
+ "\u2820\u2810\u2808\u2801",
344
+ "\u2810\u2808\u2801\u2802",
345
+ "\u2808\u2801\u2802\u2804"
346
+ ],
347
+ interval: 100
348
+ },
349
+ dna: {
350
+ frames: [
351
+ "\u280B\u2809\u2819\u281A",
352
+ "\u2809\u2819\u281A\u2812",
353
+ "\u2819\u281A\u2812\u2802",
354
+ "\u281A\u2812\u2802\u2802",
355
+ "\u2812\u2802\u2802\u2812",
356
+ "\u2802\u2802\u2812\u2832",
357
+ "\u2802\u2812\u2832\u2834",
358
+ "\u2812\u2832\u2834\u2824",
359
+ "\u2832\u2834\u2824\u2804",
360
+ "\u2834\u2824\u2804\u280B",
361
+ "\u2824\u2804\u280B\u2809",
362
+ "\u2804\u280B\u2809\u2819"
363
+ ],
364
+ interval: 80
365
+ },
366
+ // === Generated braille grid animations ===
367
+ scan: { frames: genScan(), interval: 70 },
368
+ rain: { frames: genRain(), interval: 100 },
369
+ scanline: { frames: genScanLine(), interval: 120 },
370
+ pulse: { frames: genPulse(), interval: 180 },
371
+ snake: { frames: genSnake(), interval: 80 },
372
+ sparkle: { frames: genSparkle(), interval: 150 },
373
+ cascade: { frames: genCascade(), interval: 60 },
374
+ columns: { frames: genColumns(), interval: 60 },
375
+ orbit: { frames: genOrbit(), interval: 100 },
376
+ breathe: { frames: genBreathe(), interval: 100 },
377
+ waverows: { frames: genWaveRows(), interval: 90 },
378
+ checkerboard: { frames: genCheckerboard(), interval: 250 },
379
+ helix: { frames: genHelix(), interval: 80 },
380
+ fillsweep: { frames: genFillSweep(), interval: 100 },
381
+ diagswipe: { frames: genDiagonalSwipe(), interval: 60 }
382
+ };
383
+ var braille_default = spinners;
384
+ return __toCommonJS(braille_exports);
385
+ })();
package/dist/braille.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-JW3PMLWA.js";
6
+ } from "./chunk-F2BWZODB.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
@@ -29,6 +29,7 @@ function gridToBraille(grid) {
29
29
  return result;
30
30
  }
31
31
  function makeGrid(rows, cols) {
32
+ if (rows <= 0 || cols <= 0) return [];
32
33
  return Array.from({ length: rows }, () => Array(cols).fill(false));
33
34
  }
34
35
  function genScan() {
@@ -350,24 +351,7 @@ var spinners = {
350
351
  checkerboard: { frames: genCheckerboard(), interval: 250 },
351
352
  helix: { frames: genHelix(), interval: 80 },
352
353
  fillsweep: { frames: genFillSweep(), interval: 100 },
353
- diagswipe: { frames: genDiagonalSwipe(), interval: 60 },
354
- // === Non-braille classics ===
355
- arc: {
356
- frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"],
357
- interval: 100
358
- },
359
- halfmoon: {
360
- frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"],
361
- interval: 180
362
- },
363
- line: {
364
- frames: ["|", "/", "\u2014", "\\"],
365
- interval: 100
366
- },
367
- blocks: {
368
- frames: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588", "\u2587", "\u2586", "\u2585", "\u2584", "\u2583", "\u2582"],
369
- interval: 100
370
- }
354
+ diagswipe: { frames: genDiagonalSwipe(), interval: 60 }
371
355
  };
372
356
  var braille_default = spinners;
373
357
 
package/dist/index.cjs CHANGED
@@ -58,6 +58,7 @@ function gridToBraille(grid) {
58
58
  return result;
59
59
  }
60
60
  function makeGrid(rows, cols) {
61
+ if (rows <= 0 || cols <= 0) return [];
61
62
  return Array.from({ length: rows }, () => Array(cols).fill(false));
62
63
  }
63
64
  function genScan() {
@@ -379,24 +380,7 @@ var spinners = {
379
380
  checkerboard: { frames: genCheckerboard(), interval: 250 },
380
381
  helix: { frames: genHelix(), interval: 80 },
381
382
  fillsweep: { frames: genFillSweep(), interval: 100 },
382
- diagswipe: { frames: genDiagonalSwipe(), interval: 60 },
383
- // === Non-braille classics ===
384
- arc: {
385
- frames: ["\u25DC", "\u25E0", "\u25DD", "\u25DE", "\u25E1", "\u25DF"],
386
- interval: 100
387
- },
388
- halfmoon: {
389
- frames: ["\u25D0", "\u25D3", "\u25D1", "\u25D2"],
390
- interval: 180
391
- },
392
- line: {
393
- frames: ["|", "/", "\u2014", "\\"],
394
- interval: 100
395
- },
396
- blocks: {
397
- frames: ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588", "\u2587", "\u2586", "\u2585", "\u2584", "\u2583", "\u2582"],
398
- interval: 100
399
- }
383
+ diagswipe: { frames: genDiagonalSwipe(), interval: 60 }
400
384
  };
401
385
  var braille_default = spinners;
402
386
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-JW3PMLWA.js";
6
+ } from "./chunk-F2BWZODB.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
package/package.json CHANGED
@@ -1,16 +1,28 @@
1
1
  {
2
2
  "name": "unicode-animations",
3
- "version": "0.2.1",
3
+ "version": "1.0.0",
4
4
  "description": "Unicode spinner animations as raw frame data",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
9
- "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
8
+ "import": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "require": {
13
+ "types": "./dist/index.d.cts",
14
+ "default": "./dist/index.cjs"
15
+ }
10
16
  },
11
17
  "./braille": {
12
- "import": { "types": "./dist/braille.d.ts", "default": "./dist/braille.js" },
13
- "require": { "types": "./dist/braille.d.cts", "default": "./dist/braille.cjs" }
18
+ "import": {
19
+ "types": "./dist/braille.d.ts",
20
+ "default": "./dist/braille.js"
21
+ },
22
+ "require": {
23
+ "types": "./dist/braille.d.cts",
24
+ "default": "./dist/braille.cjs"
25
+ }
14
26
  }
15
27
  },
16
28
  "bin": {
@@ -19,10 +31,14 @@
19
31
  "main": "./dist/index.cjs",
20
32
  "module": "./dist/index.js",
21
33
  "types": "./dist/index.d.ts",
22
- "files": ["dist", "scripts"],
34
+ "files": [
35
+ "dist",
36
+ "scripts"
37
+ ],
23
38
  "sideEffects": false,
24
39
  "scripts": {
25
40
  "build": "tsup",
41
+ "test": "vitest run",
26
42
  "postinstall": "node scripts/postinstall.cjs",
27
43
  "prepublishOnly": "npm run build"
28
44
  },
@@ -31,10 +47,19 @@
31
47
  "url": "git+https://github.com/gunnargray-dev/unicode-animations.git"
32
48
  },
33
49
  "homepage": "https://github.com/gunnargray-dev/unicode-animations",
34
- "keywords": ["braille", "spinner", "unicode", "animation", "loading", "cli", "terminal"],
50
+ "keywords": [
51
+ "braille",
52
+ "spinner",
53
+ "unicode",
54
+ "animation",
55
+ "loading",
56
+ "cli",
57
+ "terminal"
58
+ ],
35
59
  "license": "MIT",
36
60
  "devDependencies": {
37
61
  "tsup": "^8.4.0",
38
- "typescript": "^5.7.0"
62
+ "typescript": "^5.7.0",
63
+ "vitest": "^3.0.0"
39
64
  }
40
65
  }
package/scripts/demo.cjs CHANGED
@@ -34,7 +34,7 @@ if (!out.isTTY) {
34
34
  out = new tty.WriteStream(fd);
35
35
  } catch {
36
36
  // Fallback: no TTY available, just list and exit
37
- console.log('22 spinners: ' + names.join(', '));
37
+ console.log('18 spinners: ' + names.join(', '));
38
38
  process.exit(0);
39
39
  }
40
40
  }
@@ -51,9 +51,23 @@ const cleanup = () => { try { out.write(show); } catch {} };
51
51
  process.on('SIGINT', () => { cleanup(); out.write('\n'); process.exit(0); });
52
52
  process.on('exit', cleanup);
53
53
 
54
+ // Enable raw mode so keypresses (q, Ctrl+C, Esc) are caught immediately
55
+ if (process.stdin.isTTY) {
56
+ process.stdin.setRawMode(true);
57
+ process.stdin.resume();
58
+ process.stdin.on('data', (key) => {
59
+ // q, Ctrl+C, or Escape
60
+ if (key[0] === 0x71 || key[0] === 0x03 || key[0] === 0x1B) {
61
+ cleanup();
62
+ out.write('\n');
63
+ process.exit(0);
64
+ }
65
+ });
66
+ }
67
+
54
68
  if (args[0] === '--list' || args[0] === '-l') {
55
69
  cleanup();
56
- out.write(`\n${bold}22 spinners available:${reset}\n\n`);
70
+ out.write(`\n${bold}18 spinners available:${reset}\n\n`);
57
71
  for (const name of names) {
58
72
  const s = S[name];
59
73
  out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
package/scripts/demo.html CHANGED
@@ -155,6 +155,59 @@
155
155
  }
156
156
  footer a { color: var(--accent); text-decoration: none; }
157
157
  footer a:hover { text-decoration: underline; }
158
+
159
+ .prose {
160
+ font-size: 0.9rem;
161
+ line-height: 1.7;
162
+ color: var(--text-2);
163
+ margin-bottom: 1rem;
164
+ }
165
+ .prose code, .sub-label code, .ref-table code {
166
+ font-family: var(--mono);
167
+ font-size: 0.8rem;
168
+ background: var(--surface);
169
+ border: 1px solid var(--border);
170
+ border-radius: 4px;
171
+ padding: 0.15rem 0.4rem;
172
+ }
173
+ .ref-table code { font-size: 0.75rem; }
174
+
175
+ .doc-section {
176
+ margin-top: 3rem;
177
+ }
178
+ .doc-section .usage {
179
+ margin-top: 1rem;
180
+ }
181
+
182
+ .sub-label {
183
+ font-size: 0.8rem;
184
+ font-weight: 600;
185
+ color: var(--text);
186
+ margin: 1.5rem 0 0.75rem;
187
+ }
188
+
189
+ .ref-table {
190
+ width: 100%;
191
+ border-collapse: collapse;
192
+ font-size: 0.8rem;
193
+ margin: 1rem 0;
194
+ }
195
+ .ref-table th {
196
+ text-align: left;
197
+ font-weight: 600;
198
+ font-size: 0.7rem;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.08em;
201
+ color: var(--text-3);
202
+ padding: 0.5rem 0.75rem;
203
+ border-bottom: 1px solid var(--border);
204
+ }
205
+ .ref-table td {
206
+ padding: 0.5rem 0.75rem;
207
+ border-bottom: 1px solid var(--border);
208
+ color: var(--text-2);
209
+ }
210
+ .ref-table tr:last-child td { border-bottom: none; }
158
211
  </style>
159
212
  </head>
160
213
  <body>
@@ -194,65 +247,214 @@
194
247
  clearInterval(timer)
195
248
  process.stdout.write(<span class="str">'\r✔ Done.\n'</span>)</pre>
196
249
  </section>
250
+
251
+ <section class="doc-section">
252
+ <div class="section-label">Quick start</div>
253
+ <div class="usage">
254
+ <pre><span class="cm">// ESM</span>
255
+ <span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
256
+
257
+ <span class="cm">// CJS</span>
258
+ <span class="kw">const</span> spinners = require(<span class="str">'unicode-animations'</span>)</pre>
259
+ </div>
260
+ <p class="prose">Each spinner is a <code>{ frames: string[], interval: number }</code> object.</p>
261
+ </section>
262
+
263
+ <section class="doc-section">
264
+ <div class="section-label">Examples</div>
265
+
266
+ <div class="sub-label">CLI tool — spinner during async work</div>
267
+ <div class="usage">
268
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
269
+
270
+ <span class="kw">const</span> { frames, interval } = spinners.braille
271
+ <span class="kw">let</span> i = 0
272
+
273
+ <span class="kw">const</span> spinner = setInterval(() => {
274
+ process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} Deploying...`</span>)
275
+ }, interval)
276
+
277
+ <span class="kw">await</span> deploy()
278
+
279
+ clearInterval(spinner)
280
+ process.stdout.write(<span class="str">'\r\x1B[2K ✔ Deployed.\n'</span>)</pre>
281
+ </div>
282
+
283
+ <div class="sub-label">Reusable spinner helper</div>
284
+ <div class="usage">
285
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
286
+
287
+ <span class="kw">function</span> createSpinner(msg, name = <span class="str">'braille'</span>) {
288
+ <span class="kw">const</span> { frames, interval } = spinners[name]
289
+ <span class="kw">let</span> i = 0, text = msg
290
+ <span class="kw">const</span> timer = setInterval(() => {
291
+ process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} ${text}`</span>)
292
+ }, interval)
293
+
294
+ <span class="kw">return</span> {
295
+ update(msg) { text = msg },
296
+ stop(msg) { clearInterval(timer); process.stdout.write(<span class="str">`\r\x1B[2K ✔ ${msg}\n`</span>) },
297
+ }
298
+ }
299
+
300
+ <span class="kw">const</span> s = createSpinner(<span class="str">'Connecting to database...'</span>)
301
+ <span class="kw">const</span> db = <span class="kw">await</span> connect()
302
+ s.update(<span class="str">`Running ${migrations.length} migrations...`</span>)
303
+ <span class="kw">await</span> db.migrate(migrations)
304
+ s.stop(<span class="str">'Database ready.'</span>)</pre>
305
+ </div>
306
+
307
+ <div class="sub-label">Multi-step pipeline</div>
308
+ <div class="usage">
309
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
310
+
311
+ <span class="kw">async function</span> runWithSpinner(label, fn, name = <span class="str">'braille'</span>) {
312
+ <span class="kw">const</span> { frames, interval } = spinners[name]
313
+ <span class="kw">let</span> i = 0
314
+ <span class="kw">const</span> timer = setInterval(() => {
315
+ process.stdout.write(<span class="str">`\r\x1B[2K ${frames[i++ % frames.length]} ${label}`</span>)
316
+ }, interval)
317
+ <span class="kw">const</span> result = <span class="kw">await</span> fn()
318
+ clearInterval(timer)
319
+ process.stdout.write(<span class="str">`\r\x1B[2K ✔ ${label}\n`</span>)
320
+ <span class="kw">return</span> result
321
+ }
322
+
323
+ <span class="kw">await</span> runWithSpinner(<span class="str">'Linting...'</span>, lint, <span class="str">'scan'</span>)
324
+ <span class="kw">await</span> runWithSpinner(<span class="str">'Running tests...'</span>, test, <span class="str">'helix'</span>)
325
+ <span class="kw">await</span> runWithSpinner(<span class="str">'Building...'</span>, build, <span class="str">'cascade'</span>)
326
+ <span class="kw">await</span> runWithSpinner(<span class="str">'Publishing...'</span>, publish, <span class="str">'braille'</span>)</pre>
327
+ </div>
328
+
329
+ <div class="sub-label">React component</div>
330
+ <div class="usage">
331
+ <pre><span class="kw">import</span> { useState, useEffect } <span class="kw">from</span> <span class="str">'react'</span>
332
+ <span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
333
+
334
+ <span class="kw">function</span> Spinner({ name = <span class="str">'braille'</span>, children }) {
335
+ <span class="kw">const</span> [frame, setFrame] = useState(0)
336
+ <span class="kw">const</span> s = spinners[name]
337
+
338
+ useEffect(() => {
339
+ <span class="kw">const</span> timer = setInterval(
340
+ () => setFrame(f => (f + 1) % s.frames.length),
341
+ s.interval
342
+ )
343
+ <span class="kw">return</span> () => clearInterval(timer)
344
+ }, [name])
345
+
346
+ <span class="kw">return</span> &lt;span style={{ fontFamily: <span class="str">'monospace'</span> }}>{s.frames[frame]} {children}&lt;/span>
347
+ }
348
+
349
+ <span class="cm">// Usage: &lt;Spinner name="helix">Generating response...&lt;/Spinner></span></pre>
350
+ </div>
351
+
352
+ <div class="sub-label">Browser — status indicator</div>
353
+ <div class="usage">
354
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
355
+
356
+ <span class="kw">const</span> el = document.getElementById(<span class="str">'status'</span>)
357
+ <span class="kw">const</span> { frames, interval } = spinners.orbit
358
+ <span class="kw">let</span> i = 0
359
+
360
+ <span class="kw">const</span> spinner = setInterval(() => {
361
+ el.textContent = <span class="str">`${frames[i++ % frames.length]} Syncing...`</span>
362
+ }, interval)
363
+
364
+ <span class="kw">await</span> sync()
365
+ clearInterval(spinner)
366
+ el.textContent = <span class="str">'✔ Synced'</span></pre>
367
+ </div>
368
+ </section>
369
+
370
+ <section class="doc-section">
371
+ <div class="section-label">All spinners</div>
372
+
373
+ <div class="sub-label">Classic braille</div>
374
+ <table class="ref-table">
375
+ <thead><tr><th>Name</th><th>Preview</th><th>Interval</th></tr></thead>
376
+ <tbody>
377
+ <tr><td><code>braille</code></td><td><code>⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏</code></td><td>80ms</td></tr>
378
+ <tr><td><code>braillewave</code></td><td><code>⠁⠂⠄⡀</code> → <code>⠂⠄⡀⢀</code></td><td>100ms</td></tr>
379
+ <tr><td><code>dna</code></td><td><code>⠋⠉⠙⠚</code> → <code>⠉⠙⠚⠒</code></td><td>80ms</td></tr>
380
+ </tbody>
381
+ </table>
382
+
383
+ <div class="sub-label">Grid animations (braille)</div>
384
+ <table class="ref-table">
385
+ <thead><tr><th>Name</th><th>Frames</th><th>Interval</th></tr></thead>
386
+ <tbody>
387
+ <tr><td><code>scan</code></td><td>10</td><td>70ms</td></tr>
388
+ <tr><td><code>rain</code></td><td>12</td><td>100ms</td></tr>
389
+ <tr><td><code>scanline</code></td><td>6</td><td>120ms</td></tr>
390
+ <tr><td><code>pulse</code></td><td>5</td><td>180ms</td></tr>
391
+ <tr><td><code>snake</code></td><td>16</td><td>80ms</td></tr>
392
+ <tr><td><code>sparkle</code></td><td>6</td><td>150ms</td></tr>
393
+ <tr><td><code>cascade</code></td><td>12</td><td>60ms</td></tr>
394
+ <tr><td><code>columns</code></td><td>26</td><td>60ms</td></tr>
395
+ <tr><td><code>orbit</code></td><td>8</td><td>100ms</td></tr>
396
+ <tr><td><code>breathe</code></td><td>17</td><td>100ms</td></tr>
397
+ <tr><td><code>waverows</code></td><td>16</td><td>90ms</td></tr>
398
+ <tr><td><code>checkerboard</code></td><td>4</td><td>250ms</td></tr>
399
+ <tr><td><code>helix</code></td><td>16</td><td>80ms</td></tr>
400
+ <tr><td><code>fillsweep</code></td><td>11</td><td>100ms</td></tr>
401
+ <tr><td><code>diagswipe</code></td><td>16</td><td>60ms</td></tr>
402
+ </tbody>
403
+ </table>
404
+ </section>
405
+
406
+ <section class="doc-section">
407
+ <div class="section-label">Custom spinners</div>
408
+ <p class="prose">Create your own braille spinners using the grid utilities:</p>
409
+ <div class="usage">
410
+ <pre><span class="kw">import</span> { gridToBraille, makeGrid } <span class="kw">from</span> <span class="str">'unicode-animations'</span>
411
+
412
+ <span class="cm">// Create a 4-row × 4-col grid</span>
413
+ <span class="kw">const</span> grid = makeGrid(4, 4)
414
+ grid[0][0] = <span class="kw">true</span>
415
+ grid[1][1] = <span class="kw">true</span>
416
+ grid[2][2] = <span class="kw">true</span>
417
+ grid[3][3] = <span class="kw">true</span>
418
+
419
+ console.log(gridToBraille(grid)) <span class="cm">// diagonal braille pattern</span></pre>
420
+ </div>
421
+ <p class="prose"><code>makeGrid(rows, cols)</code> returns a <code>boolean[][]</code>. Set cells to <code>true</code> to raise dots. <code>gridToBraille(grid)</code> converts it to a braille string (2 dot-columns per character).</p>
422
+ </section>
423
+
424
+ <section class="doc-section">
425
+ <div class="section-label">API</div>
426
+
427
+ <div class="sub-label">Spinner</div>
428
+ <div class="usage">
429
+ <pre><span class="kw">interface</span> Spinner {
430
+ <span class="kw">readonly</span> frames: <span class="kw">readonly</span> string[]
431
+ <span class="kw">readonly</span> interval: number
432
+ }</pre>
433
+ </div>
434
+
435
+ <div class="sub-label">Exports from <code>'unicode-animations'</code></div>
436
+ <table class="ref-table">
437
+ <thead><tr><th>Export</th><th>Type</th></tr></thead>
438
+ <tbody>
439
+ <tr><td><code>default</code> / <code>spinners</code></td><td><code>Record&lt;BrailleSpinnerName, Spinner></code></td></tr>
440
+ <tr><td><code>gridToBraille(grid)</code></td><td><code>(boolean[][]) => string</code></td></tr>
441
+ <tr><td><code>makeGrid(rows, cols)</code></td><td><code>(number, number) => boolean[][]</code></td></tr>
442
+ <tr><td><code>Spinner</code></td><td>TypeScript interface</td></tr>
443
+ <tr><td><code>BrailleSpinnerName</code></td><td>Union type of all 18 spinner names</td></tr>
444
+ </tbody>
445
+ </table>
446
+ <p class="prose">The subpath <code>'unicode-animations/braille'</code> re-exports everything from the main entrypoint.</p>
447
+ </section>
197
448
  </main>
198
449
 
199
450
  <footer>
200
- <a href="https://github.com/gunnargray-dev/unicode-animations">GitHub</a>
451
+ made by <a href="https://x.com/gunnargray">Gunnar Gray</a>
201
452
  &nbsp;·&nbsp; MIT License
202
453
  </footer>
203
454
 
455
+ <script src="../dist/braille.global.js"></script>
204
456
  <script>
205
- // Spinner data (inlined from the package)
206
- const BRAILLE_DOT_MAP = [[0x01,0x08],[0x02,0x10],[0x04,0x20],[0x40,0x80]];
207
- function gridToBraille(grid) {
208
- const rows = grid.length, cols = grid[0]?.length || 0, cc = Math.ceil(cols / 2);
209
- let r = '';
210
- for (let c = 0; c < cc; c++) {
211
- let code = 0x2800;
212
- for (let ri = 0; ri < 4 && ri < rows; ri++)
213
- for (let d = 0; d < 2; d++) { const col = c*2+d; if (col < cols && grid[ri]?.[col]) code |= BRAILLE_DOT_MAP[ri][d]; }
214
- r += String.fromCodePoint(code);
215
- }
216
- return r;
217
- }
218
- function makeGrid(r,c) { return Array.from({length:r},()=>Array(c).fill(false)); }
219
-
220
- function genScan(){const W=8,H=4,f=[];for(let p=-1;p<W+1;p++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(c===p||c===p-1)g[r][c]=true;f.push(gridToBraille(g))}return f}
221
- function genRain(){const W=8,H=4,n=12,f=[],o=[0,3,1,5,2,7,4,6];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const r=(i+o[c])%(H+2);if(r<H)g[r][c]=true}f.push(gridToBraille(g))}return f}
222
- function genScanLine(){const W=6,H=4,f=[],p=[0,1,2,3,2,1];for(const row of p){const g=makeGrid(H,W);for(let c=0;c<W;c++){g[row][c]=true;if(row>0)g[row-1][c]=(c%2===0)}f.push(gridToBraille(g))}return f}
223
- function genPulse(){const W=6,H=4,f=[],cx=W/2-0.5,cy=H/2-0.5;for(const r of[0.5,1.2,2,3,3.5]){const g=makeGrid(H,W);for(let row=0;row<H;row++)for(let col=0;col<W;col++)if(Math.abs(Math.sqrt((col-cx)**2+(row-cy)**2)-r)<0.9)g[row][col]=true;f.push(gridToBraille(g))}return f}
224
- function genSnake(){const W=4,H=4,path=[];for(let r=0;r<H;r++)if(r%2===0)for(let c=0;c<W;c++)path.push([r,c]);else for(let c=W-1;c>=0;c--)path.push([r,c]);const f=[];for(let i=0;i<path.length;i++){const g=makeGrid(H,W);for(let t=0;t<4;t++){const idx=(i-t+path.length)%path.length;g[path[idx][0]][path[idx][1]]=true}f.push(gridToBraille(g))}return f}
225
- function genSparkle(){const ps=[[1,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0,1,1,0,0,0,0,1,0,0],[0,1,0,0,1,0,0,1,1,0,0,1,0,0,0,1,0,0,0,1,0,1,0,0,0,0,1,0,1,0,1,0],[0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,1,0,0,0,0,1,0,1,0,1,0,0,0,1],[1,0,0,0,0,0,1,1,0,0,1,0,1,0,0,0,0,0,0,0,1,0,1,0,1,0,0,1,0,0,1,0],[0,0,0,1,1,0,0,0,0,1,0,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1,0,0,0,1,0,1],[0,1,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,1,0,1,0,0,0]];const W=8,H=4,f=[];for(const p of ps){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)g[r][c]=!!p[r*W+c];f.push(gridToBraille(g))}return f}
226
- function genCascade(){const W=8,H=4,f=[];for(let o=-2;o<W+H;o++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(c+r===o||c+r===o-1)g[r][c]=true;f.push(gridToBraille(g))}return f}
227
- function genColumns(){const W=6,H=4,f=[];for(let col=0;col<W;col++)for(let ft=H-1;ft>=0;ft--){const g=makeGrid(H,W);for(let pc=0;pc<col;pc++)for(let r=0;r<H;r++)g[r][pc]=true;for(let r=ft;r<H;r++)g[r][col]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));f.push(gridToBraille(makeGrid(H,W)));return f}
228
- function genOrbit(){const W=2,H=4,path=[[0,0],[0,1],[1,1],[2,1],[3,1],[3,0],[2,0],[1,0]],f=[];for(let i=0;i<path.length;i++){const g=makeGrid(H,W);g[path[i][0]][path[i][1]]=true;const t=(i-1+path.length)%path.length;g[path[t][0]][path[t][1]]=true;f.push(gridToBraille(g))}return f}
229
- function genBreathe(){const stages=[[],[[1,0]],[[0,1],[2,0]],[[0,0],[1,1],[3,0]],[[0,0],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,1],[2,0],[3,1]],[[0,0],[0,1],[1,0],[2,1],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[3,0],[3,1]],[[0,0],[0,1],[1,0],[1,1],[2,0],[2,1],[3,0],[3,1]]];const seq=[...stages,...stages.slice().reverse().slice(1)],f=[];for(const dots of seq){const g=makeGrid(4,2);for(const[r,c]of dots)g[r][c]=true;f.push(gridToBraille(g))}return f}
230
- function genWaveRows(){const W=8,H=4,n=16,f=[];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const row=Math.round((Math.sin((i-c*0.5)*0.8)+1)/2*(H-1));g[row][c]=true;if(row>0)g[row-1][c]=(i+c)%3===0}f.push(gridToBraille(g))}return f}
231
- function genCheckerboard(){const W=6,H=4,f=[];for(let p=0;p<4;p++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)g[r][c]=p<2?(r+c+p)%2===0:(r+c+p)%3===0;f.push(gridToBraille(g))}return f}
232
- function genHelix(){const W=8,H=4,n=16,f=[];for(let i=0;i<n;i++){const g=makeGrid(H,W);for(let c=0;c<W;c++){const ph=(i+c)*(Math.PI/4);g[Math.round((Math.sin(ph)+1)/2*(H-1))][c]=true;g[Math.round((Math.sin(ph+Math.PI)+1)/2*(H-1))][c]=true}f.push(gridToBraille(g))}return f}
233
- function genFillSweep(){const W=4,H=4,f=[];for(let row=H-1;row>=0;row--){const g=makeGrid(H,W);for(let r=row;r<H;r++)for(let c=0;c<W;c++)g[r][c]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));f.push(gridToBraille(full));for(let row=0;row<H;row++){const g=makeGrid(H,W);for(let r=row+1;r<H;r++)for(let c=0;c<W;c++)g[r][c]=true;f.push(gridToBraille(g))}f.push(gridToBraille(makeGrid(H,W)));return f}
234
- function genDiagSwipe(){const W=4,H=4,f=[],mx=W+H-2;for(let d=0;d<=mx;d++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(r+c<=d)g[r][c]=true;f.push(gridToBraille(g))}const full=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)full[r][c]=true;f.push(gridToBraille(full));for(let d=0;d<=mx;d++){const g=makeGrid(H,W);for(let r=0;r<H;r++)for(let c=0;c<W;c++)if(r+c>d)g[r][c]=true;f.push(gridToBraille(g))}f.push(gridToBraille(makeGrid(H,W)));return f}
235
-
236
- const spinners = {
237
- braille:{frames:['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'],interval:80},
238
- braillewave:{frames:['⠁⠂⠄⡀','⠂⠄⡀⢀','⠄⡀⢀⠠','⡀⢀⠠⠐','⢀⠠⠐⠈','⠠⠐⠈⠁','⠐⠈⠁⠂','⠈⠁⠂⠄'],interval:100},
239
- dna:{frames:['⠋⠉⠙⠚','⠉⠙⠚⠒','⠙⠚⠒⠂','⠚⠒⠂⠂','⠒⠂⠂⠒','⠂⠂⠒⠲','⠂⠒⠲⠴','⠒⠲⠴⠤','⠲⠴⠤⠄','⠴⠤⠄⠋','⠤⠄⠋⠉','⠄⠋⠉⠙'],interval:80},
240
- scan:{frames:genScan(),interval:70},
241
- rain:{frames:genRain(),interval:100},
242
- scanline:{frames:genScanLine(),interval:120},
243
- pulse:{frames:genPulse(),interval:180},
244
- snake:{frames:genSnake(),interval:80},
245
- sparkle:{frames:genSparkle(),interval:150},
246
- cascade:{frames:genCascade(),interval:60},
247
- columns:{frames:genColumns(),interval:60},
248
- orbit:{frames:genOrbit(),interval:100},
249
- breathe:{frames:genBreathe(),interval:100},
250
- waverows:{frames:genWaveRows(),interval:90},
251
- checkerboard:{frames:genCheckerboard(),interval:250},
252
- helix:{frames:genHelix(),interval:80},
253
- fillsweep:{frames:genFillSweep(),interval:100},
254
- diagswipe:{frames:genDiagSwipe(),interval:60},
255
- };
457
+ const spinners = UnicodeAnimations.spinners;
256
458
 
257
459
  // Build grid
258
460
  const grid = document.getElementById('spinnerGrid');
@@ -7,6 +7,9 @@ const path = require('path');
7
7
  const ci = process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS;
8
8
  if (ci) process.exit(0);
9
9
 
10
+ // Skip postinstall when run via npx (temporary install for CLI usage)
11
+ if (__dirname.includes('_npx')) process.exit(0);
12
+
10
13
  let out;
11
14
  try {
12
15
  const fd = fs.openSync('/dev/tty', 'w');
@@ -28,82 +31,130 @@ try {
28
31
  const DURATION = 3000;
29
32
  const INTERVAL = 80;
30
33
 
31
- const d = '\x1B[2m';
32
- const b = '\x1B[1m';
33
- const r = '\x1B[0m';
34
- const c = '\x1B[36m';
35
- const g = '\x1B[32m';
36
- const w = '\x1B[37m';
37
- const m = '\x1B[35m';
38
- const hide = '\x1B[?25l';
39
- const show = '\x1B[?25h';
40
-
41
- out.write(hide);
42
- const cleanup = () => { try { out.write(show); } catch {} };
34
+ const B = '\x1B[1m';
35
+ const D = '\x1B[2m';
36
+ const R = '\x1B[0m';
37
+ const HIDE = '\x1B[?25l';
38
+ const SHOW = '\x1B[?25h';
39
+
40
+ out.write(HIDE);
41
+ const cleanup = () => { try { out.write(SHOW); } catch {} };
43
42
  process.on('SIGINT', () => { cleanup(); process.exit(0); });
44
43
 
45
- // Header
46
- out.write(`
47
- ${b}${w} ██╗ ██╗███╗ ██╗██╗ ██████╗ ██████╗ ██████╗ ███████╗${r}
48
- ${b}${w} ██║ ██║████╗ ██║██║██╔════╝██╔═══██╗██╔══██╗██╔════╝${r}
49
- ${b}${w} ██║ ██║██╔██╗ ██║██║██║ ██║ ██║██║ ██║█████╗${r}
50
- ${b}${w} ██║ ██║██║╚██╗██║██║██║ ██║ ██║██║ ██║██╔══╝${r}
51
- ${b}${w} ╚██████╔╝██║ ╚████║██║╚██████╗╚██████╔╝██████╔╝███████╗${r}
52
- ${d} ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${r}
53
- ${b}${c} b r a i l l e a n i m a t i o n s${r}
54
-
55
- `);
56
-
57
- // Braille spinners only, 2 columns of 9
58
- const left = [
59
- ['Braille', S.braille],
60
- ['Orbit', S.orbit],
61
- ['Breathe', S.breathe],
62
- ['Snake', S.snake],
63
- ['Fill Sweep', S.fillsweep],
64
- ['Diag Swipe', S.diagswipe],
65
- ['Pulse', S.pulse],
66
- ['Scanline', S.scanline],
67
- ['Columns', S.columns],
68
- ];
44
+ // Narrow terminal fallback
45
+ const termCols = out.columns || 80;
46
+ if (termCols < 60) {
47
+ out.write(`\n ${B}unicode-animations${R} ${D}— 18 braille spinners${R}\n\n`);
48
+ cleanup();
49
+ return;
50
+ }
69
51
 
70
- const right = [
71
- ['Checkerboard', S.checkerboard],
72
- ['Scan', S.scan],
73
- ['Rain', S.rain],
74
- ['Sparkle', S.sparkle],
75
- ['Cascade', S.cascade],
76
- ['Wave Rows', S.waverows],
77
- ['Helix', S.helix],
78
- ['Braille Wave', S.braillewave],
79
- ['DNA', S.dna],
80
- ];
52
+ function pad(str, n) { return str + ' '.repeat(Math.max(0, n - str.length)); }
81
53
 
82
- const ROWS = left.length;
54
+ // ─── Title (box-drawing art) ───
55
+ const titleLines = [
56
+ '██╗ ██╗███╗ ██╗██╗ ██████╗ ██████╗ ██████╗ ███████╗',
57
+ '██║ ██║████╗ ██║██║██╔════╝██╔═══██╗██╔══██╗██╔════╝',
58
+ '██║ ██║██╔██╗ ██║██║██║ ██║ ██║██║ ██║█████╗ ',
59
+ '██║ ██║██║╚██╗██║██║██║ ██║ ██║██║ ██║██╔══╝ ',
60
+ '╚██████╔╝██║ ╚████║██║╚██████╗╚██████╔╝██████╔╝███████╗',
61
+ ' ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝',
62
+ ];
63
+ const titleW = 57;
64
+
65
+ // ─── Spinner grid: 3 cols × 6 rows ───
66
+ const layout = [
67
+ ['braille', 'scan', 'rain'],
68
+ ['orbit', 'pulse', 'sparkle'],
69
+ ['breathe', 'cascade', 'waverows'],
70
+ ['snake', 'columns', 'helix'],
71
+ ['fillsweep', 'scanline', 'braillewave'],
72
+ ['diagswipe', 'checkerboard', 'dna'],
73
+ ];
74
+ const NPAD = 13;
75
+
76
+ // Compute max frame width per column for consistent spacing
77
+ const colFPad = [0, 1, 2].map(c => {
78
+ let max = 0;
79
+ for (const row of layout) {
80
+ const sp = S[row[c]];
81
+ for (const f of sp.frames) max = Math.max(max, [...f].length);
82
+ }
83
+ return max;
84
+ });
85
+ const GRID_W = colFPad.reduce((sum, fp) => sum + fp + 1 + NPAD, 0) + 4;
86
+ const CONTENT_W = Math.max(GRID_W, titleW) + 4;
87
+
88
+ // ─── Crop marks ───
89
+ const ARM = 1;
90
+ const inner = Math.max(0, CONTENT_W - 2 - ARM * 2);
91
+ const cropPad = ' ';
92
+ const topCrop = cropPad + '\u280F' + '\u2809'.repeat(ARM) + ' '.repeat(inner) + '\u2809'.repeat(ARM) + '\u28B9';
93
+ const botCrop = cropPad + '\u28C7' + '\u28C0'.repeat(ARM) + ' '.repeat(inner) + '\u28C0'.repeat(ARM) + '\u28F8';
94
+
95
+ // Center each element within the crop frame
96
+ function centerPad(w) {
97
+ return cropPad + ' '.repeat(Math.max(0, Math.floor((CONTENT_W - w) / 2)));
98
+ }
99
+ // Left-align all content to the same column, centered as a block within crops
100
+ const contentW = Math.max(GRID_W, titleW);
101
+ const contentPad = cropPad + ' '.repeat(Math.max(0, Math.floor((CONTENT_W - contentW) / 2)));
83
102
 
84
- function pad(str, n) { return str + ' '.repeat(Math.max(0, n - str.length)); }
103
+ // ─── Render spinner grid ───
104
+ const ROWS = layout.length;
85
105
 
86
106
  function renderGrid(tick) {
87
107
  let buf = '';
88
- for (let i = 0; i < ROWS; i++) {
89
- const [ln, ls] = left[i];
90
- const [rn, rs] = right[i];
91
- const lf = ls.frames[tick % ls.frames.length];
92
- const rf = rs.frames[tick % rs.frames.length];
93
- buf += ` ${m}${pad(lf, 3)}${r} ${d}${pad(ln, 12)}${r} ${m}${pad(rf, 12)}${r} ${d}${rn}${r}\n`;
108
+ for (const row of layout) {
109
+ let line = contentPad;
110
+ for (let c = 0; c < 3; c++) {
111
+ const name = row[c];
112
+ const sp = S[name];
113
+ const frame = sp.frames[tick % sp.frames.length];
114
+ line += B + pad(frame, colFPad[c]) + R + ' ' + D + pad(name, NPAD) + R;
115
+ if (c < 2) line += ' ';
116
+ }
117
+ buf += line + '\n';
94
118
  }
95
119
  return buf;
96
120
  }
97
121
 
122
+ // ─── Print static top ───
123
+ let top = '\n';
124
+ top += topCrop + '\n';
125
+ top += '\n';
126
+ for (let i = 0; i < titleLines.length; i++) {
127
+ const style = i === titleLines.length - 1 ? D : B;
128
+ top += contentPad + style + titleLines[i] + R + '\n';
129
+ }
130
+ top += contentPad + D + 'BRAILLE ANIMATIONS' + R + '\n';
131
+ top += '\n';
132
+ out.write(top);
133
+
134
+ // ─── Print first frame of spinners ───
98
135
  out.write(renderGrid(0));
99
136
 
137
+ // ─── Animate ───
100
138
  let tick = 1;
101
139
  const start = Date.now();
102
140
 
103
141
  const timer = setInterval(() => {
104
142
  if (Date.now() - start >= DURATION) {
105
143
  clearInterval(timer);
106
- out.write(`\n ${g}${b}✔${r} ${b}unicode-animations${r} ${d}— 18 braille spinners + 4 classics${r}\n\n`);
144
+ // Print static bottom
145
+ let bot = '\n';
146
+ const cmds = [
147
+ ['npx unicode-animations', 'demo all spinners'],
148
+ ['npx unicode-animations --list', 'list all spinners'],
149
+ ['npx unicode-animations --web', 'open in browser'],
150
+ ];
151
+ for (const [left, right] of cmds) {
152
+ const gap = ' '.repeat(Math.max(2, contentW - left.length - right.length));
153
+ bot += contentPad + D + left + R + gap + D + right + R + '\n';
154
+ }
155
+ bot += '\n';
156
+ bot += botCrop + '\n\n';
157
+ out.write(bot);
107
158
  cleanup();
108
159
  return;
109
160
  }
@@ -111,6 +162,7 @@ ${b}${c} b r a i l l e a n i m a t i o n s${r}
111
162
  out.write(renderGrid(tick));
112
163
  tick++;
113
164
  }, INTERVAL);
165
+
114
166
  } catch {
115
167
  try { out.write('\x1B[?25h'); } catch {}
116
168
  process.exit(0);