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 +68 -78
- package/dist/braille.cjs +20 -20
- package/dist/braille.js +1 -1
- package/dist/{chunk-MLXIK7E7.js → chunk-JW3PMLWA.js} +20 -20
- package/dist/index.cjs +20 -20
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/scripts/demo.cjs +34 -15
- package/scripts/demo.html +310 -0
- package/scripts/postinstall.cjs +1 -1
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
|
-
##
|
|
5
|
+
## Demo
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
###
|
|
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]}
|
|
45
|
+
process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} Deploying to production...`);
|
|
45
46
|
}, interval);
|
|
46
47
|
|
|
47
|
-
await
|
|
48
|
+
await deploy();
|
|
48
49
|
|
|
49
50
|
clearInterval(spinner);
|
|
50
|
-
process.stdout.write('\r ✔
|
|
51
|
+
process.stdout.write('\r\x1B[2K ✔ Deployed.\n');
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
###
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
###
|
|
79
|
+
### Multi-step pipeline
|
|
69
80
|
|
|
70
81
|
```js
|
|
71
82
|
import spinners from 'unicode-animations';
|
|
72
83
|
|
|
73
|
-
function
|
|
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]} ${
|
|
88
|
+
process.stdout.write(`\r\x1B[2K ${frames[i++ % frames.length]} ${label}`);
|
|
78
89
|
}, interval);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await
|
|
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
|
-
###
|
|
102
|
+
### React component
|
|
97
103
|
|
|
98
|
-
```
|
|
104
|
+
```jsx
|
|
105
|
+
import { useState, useEffect } from 'react';
|
|
99
106
|
import spinners from 'unicode-animations';
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
function Spinner({ name = 'braille', children }) {
|
|
109
|
+
const [frame, setFrame] = useState(0);
|
|
110
|
+
const s = spinners[name];
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 —
|
|
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.
|
|
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]}
|
|
136
|
+
el.textContent = `${frames[i++ % frames.length]} Syncing...`;
|
|
124
137
|
}, interval);
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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` |
|
|
162
|
-
| `dna` |
|
|
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
|
|
338
|
-
"\
|
|
339
|
-
"\
|
|
340
|
-
"\u2820\u2810
|
|
341
|
-
"\u2880\u2820\u2810\u2808
|
|
342
|
-
"\
|
|
343
|
-
"\
|
|
344
|
-
"\u2802\u2804
|
|
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
|
|
351
|
-
"\u2819\u281A\u2812
|
|
352
|
-
"\
|
|
353
|
-
"\
|
|
354
|
-
"\
|
|
355
|
-
"\
|
|
356
|
-
"\
|
|
357
|
-
"\
|
|
358
|
-
"\
|
|
359
|
-
"\
|
|
360
|
-
"\
|
|
361
|
-
"\
|
|
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
|
@@ -307,31 +307,31 @@ var spinners = {
|
|
|
307
307
|
},
|
|
308
308
|
braillewave: {
|
|
309
309
|
frames: [
|
|
310
|
-
"\u2801\u2802\u2804\u2840
|
|
311
|
-
"\
|
|
312
|
-
"\
|
|
313
|
-
"\u2820\u2810
|
|
314
|
-
"\u2880\u2820\u2810\u2808
|
|
315
|
-
"\
|
|
316
|
-
"\
|
|
317
|
-
"\u2802\u2804
|
|
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
|
|
324
|
-
"\u2819\u281A\u2812
|
|
325
|
-
"\
|
|
326
|
-
"\
|
|
327
|
-
"\
|
|
328
|
-
"\
|
|
329
|
-
"\
|
|
330
|
-
"\
|
|
331
|
-
"\
|
|
332
|
-
"\
|
|
333
|
-
"\
|
|
334
|
-
"\
|
|
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
|
|
340
|
-
"\
|
|
341
|
-
"\
|
|
342
|
-
"\u2820\u2810
|
|
343
|
-
"\u2880\u2820\u2810\u2808
|
|
344
|
-
"\
|
|
345
|
-
"\
|
|
346
|
-
"\u2802\u2804
|
|
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
|
|
353
|
-
"\u2819\u281A\u2812
|
|
354
|
-
"\
|
|
355
|
-
"\
|
|
356
|
-
"\
|
|
357
|
-
"\
|
|
358
|
-
"\
|
|
359
|
-
"\
|
|
360
|
-
"\
|
|
361
|
-
"\
|
|
362
|
-
"\
|
|
363
|
-
"\
|
|
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
package/package.json
CHANGED
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
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
31
|
-
const cleanup = () =>
|
|
32
|
-
process.on('SIGINT', () => { cleanup();
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
out.write(` ${magenta}${s.frames[0]}${reset} ${name} ${dim}(${s.frames.length} frames, ${s.interval}ms)${reset}\n`);
|
|
41
60
|
}
|
|
42
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
+
· 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>
|
package/scripts/postinstall.cjs
CHANGED
|
@@ -50,7 +50,7 @@ ${b}${w} ██║ ██║██╔██╗ ██║██║██║
|
|
|
50
50
|
${b}${w} ██║ ██║██║╚██╗██║██║██║ ██║ ██║██║ ██║██╔══╝${r}
|
|
51
51
|
${b}${w} ╚██████╔╝██║ ╚████║██║╚██████╗╚██████╔╝██████╔╝███████╗${r}
|
|
52
52
|
${d} ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝${r}
|
|
53
|
-
${b}${c}
|
|
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
|
|