unicode-animations 0.1.9 → 0.2.1

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
@@ -2,14 +2,15 @@
2
2
 
3
3
  Unicode spinner animations as raw frame data — no dependencies, works everywhere.
4
4
 
5
- ## Preview
5
+ ## Demo
6
6
 
7
- ```
8
- braille ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏
9
- arc ◜ ◠ ◝ ◞ ◡ ◟
10
- halfmoon ◐
11
- blocks ▁ ▇ █ ▇ ▆ ▅ ▄ ▃ ▂
12
- line | / \
7
+ See all 22 spinners animating live:
8
+
9
+ ```bash
10
+ npx unicode-animations --web # open browser demo
11
+ npx unicode-animations # cycle through all in terminal
12
+ npx unicode-animations helix # preview a specific spinner
13
+ npx unicode-animations --list # list all spinners
13
14
  ```
14
15
 
15
16
  ## Install
@@ -32,7 +33,7 @@ Each spinner is a `{ frames: string[], interval: number }` object.
32
33
 
33
34
  ## Examples
34
35
 
35
- ### Spinner while awaiting an async task
36
+ ### CLI tool spinner during async work
36
37
 
37
38
  ```js
38
39
  import spinners from 'unicode-animations';
@@ -41,114 +42,103 @@ const { frames, interval } = spinners.braille;
41
42
  let i = 0;
42
43
 
43
44
  const spinner = setInterval(() => {
44
- process.stdout.write(`\r ${frames[i++ % frames.length]} Installing dependencies...`);
45
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} Deploying to production...`);
45
46
  }, interval);
46
47
 
47
- await install();
48
+ await deploy();
48
49
 
49
50
  clearInterval(spinner);
50
- process.stdout.write('\r ✔ Installed successfully.\n');
51
+ process.stdout.write('\r\x1B[2KDeployed.\n');
51
52
  ```
52
53
 
53
- ### Multi-character braille spinner
54
-
55
- The grid-based spinners produce wider animated patterns — useful for visual flair in CLI tools:
54
+ ### Reusable spinner helper
56
55
 
57
56
  ```js
58
57
  import spinners from 'unicode-animations';
59
58
 
60
- const { frames, interval } = spinners.helix;
61
- let i = 0;
59
+ function createSpinner(msg, name = 'braille') {
60
+ const { frames, interval } = spinners[name];
61
+ let i = 0, text = msg;
62
+ const timer = setInterval(() => {
63
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${text}`);
64
+ }, interval);
62
65
 
63
- const spinner = setInterval(() => {
64
- process.stdout.write(`\r ${frames[i++ % frames.length]} Building...`);
65
- }, interval);
66
+ return {
67
+ update(msg) { text = msg; },
68
+ stop(msg) { clearInterval(timer); process.stdout.write(`\r\x1B[2K ✔ ${msg}\n`); },
69
+ };
70
+ }
71
+
72
+ const s = createSpinner('Connecting to database...');
73
+ const db = await connect();
74
+ s.update(`Running ${migrations.length} migrations...`);
75
+ await db.migrate(migrations);
76
+ s.stop('Database ready.');
66
77
  ```
67
78
 
68
- ### Progress indicator with dynamic message
79
+ ### Multi-step pipeline
69
80
 
70
81
  ```js
71
82
  import spinners from 'unicode-animations';
72
83
 
73
- function withSpinner(message, spinner = spinners.braille) {
84
+ async function runWithSpinner(label, fn, name = 'braille') {
85
+ const { frames, interval } = spinners[name];
74
86
  let i = 0;
75
- const { frames, interval } = spinner;
76
87
  const timer = setInterval(() => {
77
- process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${message}`);
88
+ process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${label}`);
78
89
  }, interval);
79
-
80
- return {
81
- update(msg) { message = msg; },
82
- stop(finalMsg) {
83
- clearInterval(timer);
84
- process.stdout.write(`\r\x1B[2K ✔ ${finalMsg}\n`);
85
- },
86
- };
90
+ const result = await fn();
91
+ clearInterval(timer);
92
+ process.stdout.write(`\r\x1B[2K ✔ ${label}\n`);
93
+ return result;
87
94
  }
88
95
 
89
- const spin = withSpinner('Fetching data...');
90
- const data = await fetchData();
91
- spin.update(`Processing ${data.length} records...`);
92
- await processData(data);
93
- spin.stop('Done.');
96
+ await runWithSpinner('Linting...', lint, 'scan');
97
+ await runWithSpinner('Running tests...', test, 'helix');
98
+ await runWithSpinner('Building...', build, 'cascade');
99
+ await runWithSpinner('Publishing...', publish, 'braille');
94
100
  ```
95
101
 
96
- ### Cycle through different spinners
102
+ ### React component
97
103
 
98
- ```js
104
+ ```jsx
105
+ import { useState, useEffect } from 'react';
99
106
  import spinners from 'unicode-animations';
100
107
 
101
- const names = ['scan', 'rain', 'helix', 'cascade'];
102
- let current = 0;
103
- let i = 0;
108
+ function Spinner({ name = 'braille', children }) {
109
+ const [frame, setFrame] = useState(0);
110
+ const s = spinners[name];
104
111
 
105
- setInterval(() => {
106
- const s = spinners[names[current]];
107
- process.stdout.write(`\r\x1B[2K ${s.frames[i % s.frames.length]} ${names[current]}`);
108
- i++;
109
- if (i % 20 === 0) current = (current + 1) % names.length;
110
- }, 80);
112
+ useEffect(() => {
113
+ const timer = setInterval(
114
+ () => setFrame(f => (f + 1) % s.frames.length),
115
+ s.interval
116
+ );
117
+ return () => clearInterval(timer);
118
+ }, [name]);
119
+
120
+ return <span style={{ fontFamily: 'monospace' }}>{s.frames[frame]} {children}</span>;
121
+ }
122
+
123
+ // Usage: <Spinner name="helix">Generating response...</Spinner>
111
124
  ```
112
125
 
113
- ### Browser — inline loading text
126
+ ### Browser — status indicator
114
127
 
115
128
  ```js
116
129
  import spinners from 'unicode-animations';
117
130
 
118
131
  const el = document.getElementById('status');
119
- const { frames, interval } = spinners.arc;
132
+ const { frames, interval } = spinners.orbit;
120
133
  let i = 0;
121
134
 
122
135
  const spinner = setInterval(() => {
123
- el.textContent = `${frames[i++ % frames.length]} Loading...`;
136
+ el.textContent = `${frames[i++ % frames.length]} Syncing...`;
124
137
  }, interval);
125
138
 
126
- // Stop when done
127
- function onLoaded() {
128
- clearInterval(spinner);
129
- el.textContent = '✔ Ready';
130
- }
131
- ```
132
-
133
- ### React component
134
-
135
- ```jsx
136
- import { useState, useEffect } from 'react';
137
- import spinners from 'unicode-animations';
138
-
139
- function Spinner({ name = 'braille', text = 'Loading...' }) {
140
- const [frame, setFrame] = useState(0);
141
- const spinner = spinners[name];
142
-
143
- useEffect(() => {
144
- const timer = setInterval(() => {
145
- setFrame(f => (f + 1) % spinner.frames.length);
146
- }, spinner.interval);
147
- return () => clearInterval(timer);
148
- }, [name]);
149
-
150
- return <span>{spinner.frames[frame]} {text}</span>;
151
- }
139
+ await sync();
140
+ clearInterval(spinner);
141
+ el.textContent = '✔ Synced';
152
142
  ```
153
143
 
154
144
  ## All spinners
@@ -158,8 +148,8 @@ function Spinner({ name = 'braille', text = 'Loading...' }) {
158
148
  | Name | Preview | Interval |
159
149
  |------|---------|----------|
160
150
  | `braille` | `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` | 80ms |
161
- | `braillewave` | `⠁⠂⠄⡀⢀⠠⠐⠈``⠂⠄⡀⢀⠠⠐⠈⠁` | 100ms |
162
- | `dna` | `⠋⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄``⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠠` | 80ms |
151
+ | `braillewave` | `⠁⠂⠄⡀``⠂⠄⡀⢀` | 100ms |
152
+ | `dna` | `⠋⠉⠙⠚``⠉⠙⠚⠒` | 80ms |
163
153
 
164
154
  ### Grid animations (braille)
165
155
 
package/dist/braille.cjs CHANGED
@@ -334,31 +334,31 @@ var spinners = {
334
334
  },
335
335
  braillewave: {
336
336
  frames: [
337
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
338
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
339
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
340
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
341
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
342
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
343
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
344
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
337
+ "\u2801\u2802\u2804\u2840",
338
+ "\u2802\u2804\u2840\u2880",
339
+ "\u2804\u2840\u2880\u2820",
340
+ "\u2840\u2880\u2820\u2810",
341
+ "\u2880\u2820\u2810\u2808",
342
+ "\u2820\u2810\u2808\u2801",
343
+ "\u2810\u2808\u2801\u2802",
344
+ "\u2808\u2801\u2802\u2804"
345
345
  ],
346
346
  interval: 100
347
347
  },
348
348
  dna: {
349
349
  frames: [
350
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
351
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
352
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
353
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
354
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
355
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
356
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
357
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
358
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
359
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
360
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
361
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
350
+ "\u280B\u2809\u2819\u281A",
351
+ "\u2809\u2819\u281A\u2812",
352
+ "\u2819\u281A\u2812\u2802",
353
+ "\u281A\u2812\u2802\u2802",
354
+ "\u2812\u2802\u2802\u2812",
355
+ "\u2802\u2802\u2812\u2832",
356
+ "\u2802\u2812\u2832\u2834",
357
+ "\u2812\u2832\u2834\u2824",
358
+ "\u2832\u2834\u2824\u2804",
359
+ "\u2834\u2824\u2804\u280B",
360
+ "\u2824\u2804\u280B\u2809",
361
+ "\u2804\u280B\u2809\u2819"
362
362
  ],
363
363
  interval: 80
364
364
  },
package/dist/braille.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-MLXIK7E7.js";
6
+ } from "./chunk-JW3PMLWA.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
@@ -307,31 +307,31 @@ var spinners = {
307
307
  },
308
308
  braillewave: {
309
309
  frames: [
310
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
311
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
312
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
313
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
314
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
315
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
316
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
317
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
310
+ "\u2801\u2802\u2804\u2840",
311
+ "\u2802\u2804\u2840\u2880",
312
+ "\u2804\u2840\u2880\u2820",
313
+ "\u2840\u2880\u2820\u2810",
314
+ "\u2880\u2820\u2810\u2808",
315
+ "\u2820\u2810\u2808\u2801",
316
+ "\u2810\u2808\u2801\u2802",
317
+ "\u2808\u2801\u2802\u2804"
318
318
  ],
319
319
  interval: 100
320
320
  },
321
321
  dna: {
322
322
  frames: [
323
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
324
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
325
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
326
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
327
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
328
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
329
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
330
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
331
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
332
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
333
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
334
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
323
+ "\u280B\u2809\u2819\u281A",
324
+ "\u2809\u2819\u281A\u2812",
325
+ "\u2819\u281A\u2812\u2802",
326
+ "\u281A\u2812\u2802\u2802",
327
+ "\u2812\u2802\u2802\u2812",
328
+ "\u2802\u2802\u2812\u2832",
329
+ "\u2802\u2812\u2832\u2834",
330
+ "\u2812\u2832\u2834\u2824",
331
+ "\u2832\u2834\u2824\u2804",
332
+ "\u2834\u2824\u2804\u280B",
333
+ "\u2824\u2804\u280B\u2809",
334
+ "\u2804\u280B\u2809\u2819"
335
335
  ],
336
336
  interval: 80
337
337
  },
package/dist/index.cjs CHANGED
@@ -336,31 +336,31 @@ var spinners = {
336
336
  },
337
337
  braillewave: {
338
338
  frames: [
339
- "\u2801\u2802\u2804\u2840\u2880\u2820\u2810\u2808",
340
- "\u2808\u2801\u2802\u2804\u2840\u2880\u2820\u2810",
341
- "\u2810\u2808\u2801\u2802\u2804\u2840\u2880\u2820",
342
- "\u2820\u2810\u2808\u2801\u2802\u2804\u2840\u2880",
343
- "\u2880\u2820\u2810\u2808\u2801\u2802\u2804\u2840",
344
- "\u2840\u2880\u2820\u2810\u2808\u2801\u2802\u2804",
345
- "\u2804\u2840\u2880\u2820\u2810\u2808\u2801\u2802",
346
- "\u2802\u2804\u2840\u2880\u2820\u2810\u2808\u2801"
339
+ "\u2801\u2802\u2804\u2840",
340
+ "\u2802\u2804\u2840\u2880",
341
+ "\u2804\u2840\u2880\u2820",
342
+ "\u2840\u2880\u2820\u2810",
343
+ "\u2880\u2820\u2810\u2808",
344
+ "\u2820\u2810\u2808\u2801",
345
+ "\u2810\u2808\u2801\u2802",
346
+ "\u2808\u2801\u2802\u2804"
347
347
  ],
348
348
  interval: 100
349
349
  },
350
350
  dna: {
351
351
  frames: [
352
- "\u280B\u2809\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804",
353
- "\u2819\u281A\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820",
354
- "\u2839\u2812\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820",
355
- "\u2838\u2802\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804",
356
- "\u283C\u2802\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824",
357
- "\u2834\u2812\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834",
358
- "\u2826\u2832\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832",
359
- "\u2827\u2834\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812",
360
- "\u2807\u2824\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802",
361
- "\u280F\u2804\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802",
362
- "\u280B\u2804\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812",
363
- "\u2809\u2820\u2820\u2804\u2824\u2834\u2832\u2812\u2802\u2802\u2812\u2832"
352
+ "\u280B\u2809\u2819\u281A",
353
+ "\u2809\u2819\u281A\u2812",
354
+ "\u2819\u281A\u2812\u2802",
355
+ "\u281A\u2812\u2802\u2802",
356
+ "\u2812\u2802\u2802\u2812",
357
+ "\u2802\u2802\u2812\u2832",
358
+ "\u2802\u2812\u2832\u2834",
359
+ "\u2812\u2832\u2834\u2824",
360
+ "\u2832\u2834\u2824\u2804",
361
+ "\u2834\u2824\u2804\u280B",
362
+ "\u2824\u2804\u280B\u2809",
363
+ "\u2804\u280B\u2809\u2819"
364
364
  ],
365
365
  interval: 80
366
366
  },
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  gridToBraille,
4
4
  makeGrid,
5
5
  spinners
6
- } from "./chunk-MLXIK7E7.js";
6
+ } from "./chunk-JW3PMLWA.js";
7
7
  export {
8
8
  braille_default as default,
9
9
  gridToBraille,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unicode-animations",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "Unicode spinner animations as raw frame data",
5
5
  "type": "module",
6
6
  "exports": {
package/scripts/demo.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const path = require('path');
4
+ const fs = require('fs');
5
+ const tty = require('tty');
4
6
 
5
7
  let S;
6
8
  try {
@@ -14,38 +16,55 @@ try {
14
16
  const names = Object.keys(S);
15
17
  const args = process.argv.slice(2);
16
18
 
17
- // Usage: npx unicode-animations [name]
18
- // No args = cycle through all spinners
19
- // With name = show that specific spinner
19
+ // --web: open browser demo
20
+ if (args[0] === '--web' || args[0] === '-w') {
21
+ const { exec } = require('child_process');
22
+ const demoPath = path.join(__dirname, 'demo.html');
23
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
24
+ exec(`${cmd} "${demoPath}"`);
25
+ console.log(`Opening ${demoPath}`);
26
+ process.exit(0);
27
+ }
28
+
29
+ // Get a writable TTY stream — stdout if it's a TTY, otherwise /dev/tty
30
+ let out = process.stdout;
31
+ if (!out.isTTY) {
32
+ try {
33
+ const fd = fs.openSync('/dev/tty', 'w');
34
+ out = new tty.WriteStream(fd);
35
+ } catch {
36
+ // Fallback: no TTY available, just list and exit
37
+ console.log('22 spinners: ' + names.join(', '));
38
+ process.exit(0);
39
+ }
40
+ }
20
41
 
21
42
  const hide = '\x1B[?25l';
22
43
  const show = '\x1B[?25h';
23
- const clear = '\x1B[2K\r';
24
44
  const bold = '\x1B[1m';
25
45
  const dim = '\x1B[2m';
26
- const cyan = '\x1B[36m';
27
46
  const magenta = '\x1B[35m';
28
47
  const reset = '\x1B[0m';
29
48
 
30
- process.stdout.write(hide);
31
- const cleanup = () => process.stdout.write(show);
32
- process.on('SIGINT', () => { cleanup(); console.log(); process.exit(0); });
49
+ out.write(hide);
50
+ const cleanup = () => { try { out.write(show); } catch {} };
51
+ process.on('SIGINT', () => { cleanup(); out.write('\n'); process.exit(0); });
33
52
  process.on('exit', cleanup);
34
53
 
35
54
  if (args[0] === '--list' || args[0] === '-l') {
36
55
  cleanup();
37
- console.log(`\n${bold}22 spinners available:${reset}\n`);
56
+ out.write(`\n${bold}22 spinners available:${reset}\n\n`);
38
57
  for (const name of names) {
39
58
  const s = S[name];
40
- console.log(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}`);
59
+ out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
41
60
  }
42
- console.log();
61
+ out.write('\n');
43
62
  process.exit(0);
44
63
  }
45
64
 
46
65
  if (args[0] && !names.includes(args[0])) {
47
66
  cleanup();
48
- console.error(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.`);
67
+ out.write(`Unknown spinner: "${args[0]}"\nRun with --list to see all spinners.\n`);
49
68
  process.exit(1);
50
69
  }
51
70
 
@@ -54,15 +73,15 @@ const single = !!args[0];
54
73
  let i = 0;
55
74
  let ticksOnCurrent = 0;
56
75
 
57
- const TICKS_PER_SPINNER = 40; // ~3.2s per spinner when cycling
76
+ const TICKS_PER_SPINNER = 40;
58
77
 
59
78
  const timer = setInterval(() => {
60
79
  const name = names[current];
61
80
  const s = S[name];
62
81
  const frame = s.frames[i % s.frames.length];
63
- const count = `${dim}[${current + 1}/${names.length}]${reset}`;
82
+ const count = single ? '' : `${dim}[${current + 1}/${names.length}]${reset}`;
64
83
 
65
- process.stdout.write(`${clear} ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${single ? '' : count}`);
84
+ out.write(`\r\x1B[2K ${magenta}${frame}${reset} ${bold}${name}${reset} ${dim}${s.interval}ms${reset} ${count}`);
66
85
 
67
86
  i++;
68
87
  ticksOnCurrent++;
@@ -0,0 +1,310 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>unicode-animations</title>
7
+ <style>
8
+ :root {
9
+ --bg: #131316;
10
+ --surface: #1e1e23;
11
+ --border: #2e2e36;
12
+ --text: #d8d8df;
13
+ --text-2: #a4a4b0;
14
+ --text-3: #6e6e80;
15
+ --accent: #a78bfa;
16
+ --mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'Menlo', monospace;
17
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ }
19
+ [data-theme="light"] {
20
+ --bg: #f7f7fa;
21
+ --surface: #ffffff;
22
+ --border: #e8e8ec;
23
+ --text: #2c2c3a;
24
+ --text-2: #6b6b7b;
25
+ --text-3: #9d9daa;
26
+ --accent: #7c3aed;
27
+ }
28
+
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
30
+ body {
31
+ font-family: var(--sans);
32
+ background: var(--bg);
33
+ color: var(--text);
34
+ -webkit-font-smoothing: antialiased;
35
+ transition: background 0.3s, color 0.3s;
36
+ }
37
+
38
+ .theme-toggle {
39
+ position: fixed; top: 1rem; right: 1.5rem; z-index: 100;
40
+ background: var(--surface); border: 1px solid var(--border);
41
+ border-radius: 8px; padding: 0.4rem 0.75rem;
42
+ cursor: pointer; font-size: 0.75rem; font-weight: 500;
43
+ color: var(--text-2); display: flex; align-items: center; gap: 0.4rem;
44
+ transition: border-color 0.2s;
45
+ }
46
+ .theme-toggle:hover { border-color: var(--text-2); }
47
+
48
+ header {
49
+ text-align: center;
50
+ padding: 4rem 2rem 1rem;
51
+ }
52
+ header h1 {
53
+ font-size: 2rem;
54
+ font-weight: 600;
55
+ letter-spacing: -0.03em;
56
+ }
57
+ header p {
58
+ margin-top: 0.4rem;
59
+ color: var(--text-3);
60
+ font-size: 0.95rem;
61
+ }
62
+
63
+ .install {
64
+ text-align: center;
65
+ margin: 1.5rem 0 3rem;
66
+ }
67
+ .install code {
68
+ font-family: var(--mono);
69
+ font-size: 0.85rem;
70
+ background: var(--surface);
71
+ border: 1px solid var(--border);
72
+ border-radius: 8px;
73
+ padding: 0.5rem 1.25rem;
74
+ color: var(--text-2);
75
+ }
76
+
77
+ main {
78
+ max-width: 720px;
79
+ margin: 0 auto;
80
+ padding: 0 1.5rem 6rem;
81
+ }
82
+
83
+ .section-label {
84
+ font-size: 0.65rem;
85
+ font-weight: 600;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.14em;
88
+ color: var(--text-3);
89
+ margin-bottom: 1rem;
90
+ }
91
+
92
+ .grid {
93
+ display: grid;
94
+ grid-template-columns: 1fr 1fr;
95
+ gap: 0;
96
+ }
97
+ @media (max-width: 520px) { .grid { grid-template-columns: 1fr; } }
98
+
99
+ .spinner-row {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 0.75rem;
103
+ padding: 0.6rem 1rem;
104
+ border-bottom: 1px solid var(--border);
105
+ transition: background 0.15s;
106
+ }
107
+ .spinner-row:hover { background: var(--surface); }
108
+
109
+ .spinner-frame {
110
+ font-family: var(--mono);
111
+ font-size: 1rem;
112
+ width: 5rem;
113
+ text-align: center;
114
+ flex-shrink: 0;
115
+ color: var(--accent);
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .spinner-name {
120
+ font-weight: 500;
121
+ font-size: 0.85rem;
122
+ white-space: nowrap;
123
+ }
124
+
125
+ .spinner-meta {
126
+ font-size: 0.75rem;
127
+ color: var(--text-3);
128
+ margin-left: auto;
129
+ white-space: nowrap;
130
+ }
131
+
132
+ .usage {
133
+ margin-top: 3rem;
134
+ }
135
+ .usage pre {
136
+ font-family: var(--mono);
137
+ font-size: 0.8rem;
138
+ line-height: 1.6;
139
+ background: var(--surface);
140
+ border: 1px solid var(--border);
141
+ border-radius: 12px;
142
+ padding: 1.25rem 1.5rem;
143
+ overflow-x: auto;
144
+ color: var(--text-2);
145
+ }
146
+ .usage pre .kw { color: #c084fc; }
147
+ .usage pre .str { color: #86efac; }
148
+ .usage pre .cm { color: var(--text-3); }
149
+
150
+ footer {
151
+ text-align: center;
152
+ padding: 2rem;
153
+ font-size: 0.75rem;
154
+ color: var(--text-3);
155
+ }
156
+ footer a { color: var(--accent); text-decoration: none; }
157
+ footer a:hover { text-decoration: underline; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+
162
+ <button class="theme-toggle" id="toggle">
163
+ <span id="toggleIcon">☀</span>
164
+ <span id="toggleLabel">Light</span>
165
+ </button>
166
+
167
+ <header>
168
+ <h1>unicode-animations</h1>
169
+ <p>18 braille spinner animations as raw frame data</p>
170
+ </header>
171
+
172
+ <div class="install">
173
+ <code>npm install unicode-animations</code>
174
+ </div>
175
+
176
+ <main>
177
+ <section>
178
+ <div class="section-label">All braille spinners</div>
179
+ <div class="grid" id="spinnerGrid"></div>
180
+ </section>
181
+
182
+ <section class="usage">
183
+ <div class="section-label" style="margin-bottom: 1rem;">Usage</div>
184
+ <pre><span class="kw">import</span> spinners <span class="kw">from</span> <span class="str">'unicode-animations'</span>
185
+
186
+ <span class="kw">const</span> { frames, interval } = spinners.braille
187
+ <span class="kw">let</span> i = 0
188
+
189
+ <span class="kw">const</span> timer = setInterval(() => {
190
+ process.stdout.write(<span class="str">`\r${frames[i++ % frames.length]} Loading...`</span>)
191
+ }, interval)
192
+
193
+ <span class="kw">await</span> doWork()
194
+ clearInterval(timer)
195
+ process.stdout.write(<span class="str">'\r✔ Done.\n'</span>)</pre>
196
+ </section>
197
+ </main>
198
+
199
+ <footer>
200
+ <a href="https://github.com/gunnargray-dev/unicode-animations">GitHub</a>
201
+ &nbsp;·&nbsp; MIT License
202
+ </footer>
203
+
204
+ <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
+ };
256
+
257
+ // Build grid
258
+ const grid = document.getElementById('spinnerGrid');
259
+ const els = {};
260
+
261
+ Object.entries(spinners).forEach(([name, s]) => {
262
+ const row = document.createElement('div');
263
+ row.className = 'spinner-row';
264
+
265
+ const frame = document.createElement('span');
266
+ frame.className = 'spinner-frame';
267
+ frame.textContent = s.frames[0];
268
+
269
+ const label = document.createElement('span');
270
+ label.className = 'spinner-name';
271
+ label.textContent = name;
272
+
273
+ const meta = document.createElement('span');
274
+ meta.className = 'spinner-meta';
275
+ meta.textContent = `${s.frames.length}f · ${s.interval}ms`;
276
+
277
+ row.append(frame, label, meta);
278
+ grid.appendChild(row);
279
+ els[name] = frame;
280
+ });
281
+
282
+ // Animate — group by interval for efficiency
283
+ const byInterval = {};
284
+ Object.entries(spinners).forEach(([name, s]) => {
285
+ if (!byInterval[s.interval]) byInterval[s.interval] = [];
286
+ byInterval[s.interval].push({ name, frames: s.frames, i: 0 });
287
+ });
288
+
289
+ Object.entries(byInterval).forEach(([interval, group]) => {
290
+ setInterval(() => {
291
+ group.forEach(s => {
292
+ s.i = (s.i + 1) % s.frames.length;
293
+ els[s.name].textContent = s.frames[s.i];
294
+ });
295
+ }, Number(interval));
296
+ });
297
+
298
+ // Theme toggle
299
+ const toggle = document.getElementById('toggle');
300
+ const icon = document.getElementById('toggleIcon');
301
+ const label = document.getElementById('toggleLabel');
302
+ toggle.addEventListener('click', () => {
303
+ const dark = document.documentElement.dataset.theme === 'dark';
304
+ document.documentElement.dataset.theme = dark ? 'light' : 'dark';
305
+ icon.textContent = dark ? '☾' : '☀';
306
+ label.textContent = dark ? 'Dark' : 'Light';
307
+ });
308
+ </script>
309
+ </body>
310
+ </html>
@@ -50,7 +50,7 @@ ${b}${w} ██║ ██║██╔██╗ ██║██║██║
50
50
  ${b}${w} ██║ ██║██║╚██╗██║██║██║ ██║ ██║██║ ██║██╔══╝${r}
51
51
  ${b}${w} ╚██████╔╝██║ ╚████║██║╚██████╗╚██████╔╝██████╔╝███████╗${r}
52
52
  ${d} ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${r}
53
- ${b}${c} a n i m a t i o n s${r}
53
+ ${b}${c} b r a i l l e a n i m a t i o n s${r}
54
54
 
55
55
  `);
56
56