tactile-odometer 1.0.0 → 1.0.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 +176 -176
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
# tactile-odometer
|
|
2
|
-
|
|
3
|
-
A mechanical drum counter for the web that carries like the real thing.
|
|
4
|
-
|
|
5
|
-
On a real odometer a drum only moves while the one to its right is carrying,
|
|
6
|
-
so 1,099 → 1,100 rolls the last three drums over together, right at the end of
|
|
7
|
-
the units' turn. Most animated counters move every digit on its own, so
|
|
8
|
-
nothing carries. This one does.
|
|
9
|
-
|
|
10
|
-
Vanilla ES module and CSS, no dependencies, no build step. MIT.
|
|
11
|
-
|
|
12
|
-
**[Play with it →](https://tactile-ui.me/objects/odometer)**
|
|
13
|
-
|
|
14
|
-
<!-- demo gif: 1,099 → 1,100, then +37 -->
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```sh
|
|
19
|
-
npm install tactile-odometer
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Or straight from a CDN:
|
|
23
|
-
|
|
24
|
-
```html
|
|
25
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tactile-odometer/odometer.css">
|
|
26
|
-
<script type="module">
|
|
27
|
-
import Odometer from 'https://cdn.jsdelivr.net/npm/tactile-odometer/odometer.mjs';
|
|
28
|
-
</script>
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Use
|
|
32
|
-
|
|
33
|
-
```html
|
|
34
|
-
<div id="seats" style="width: 480px; height: 160px"></div>
|
|
35
|
-
|
|
36
|
-
<script type="module">
|
|
37
|
-
// with a bundler; without one, use the CDN URLs above
|
|
38
|
-
import Odometer from 'tactile-odometer';
|
|
39
|
-
import 'tactile-odometer/odometer.css';
|
|
40
|
-
|
|
41
|
-
const seats = new Odometer(document.getElementById('seats'), { value: 1099 });
|
|
42
|
-
|
|
43
|
-
seats.setValue(1100); // rolls, and carries
|
|
44
|
-
seats.setValue(4821.75); // springs there; fast drums spin, slow ones roll
|
|
45
|
-
seats.count(1); // a live tally, +1 a second (0 stops it)
|
|
46
|
-
seats.update({ finish: 'brass' });
|
|
47
|
-
seats.destroy(); // removes everything it added
|
|
48
|
-
</script>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
It works in React, Vue, Svelte or anything else that can hand it an element:
|
|
52
|
-
create it in an effect or `onMount`, call `destroy()` on unmount.
|
|
53
|
-
|
|
54
|
-
### Options
|
|
55
|
-
|
|
56
|
-
| Option | Default | What it does |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| `value` | `0` | The number to start on |
|
|
59
|
-
| `decimals` | `0` | Drums after the decimal point (0 to 6) |
|
|
60
|
-
| `prefix`, `suffix` | `''` | Printed on the plate either side of the window: `$`, `km`, `seats left` |
|
|
61
|
-
| `locale` | `'en-US'` | Group and decimal marks, grouping pattern and digit glyphs, from `Intl.NumberFormat` |
|
|
62
|
-
| `grouping` | `true` | Fixed plates between drums: 1,099 rather than 1099 |
|
|
63
|
-
| `digits` | `1` | Whole-number drums to reserve, so a growing number doesn't move the layout |
|
|
64
|
-
| `zeros` | `false` | Print 001,099 like a car odometer, instead of blanks |
|
|
65
|
-
| `finish` | `'ink'` | `ink`, `bone`, `steel` or `brass`, each in its own housing |
|
|
66
|
-
| `red` | `'none'` | Signal-red drums: `last`, or all the `decimals` (the classic tenths) |
|
|
67
|
-
| `bezel` | `'slot'` | One `slot` across the drums, `cells` like a tally counter, or `bare` to sit in a line of text |
|
|
68
|
-
| `stiffness` | `140` | How hard the value is pulled to a new number |
|
|
69
|
-
| `damping` | `0.85` | Below 1 it overshoots and winds back; 1 and above it arrives without passing |
|
|
70
|
-
| `maxSpin` | `24` | Digits per second; past this a drum is drawn spinning at the cap, blurred, instead of strobing |
|
|
71
|
-
| `count` | `0` | A live tally per second, in steps of the last drum; negative counts down |
|
|
72
|
-
| `wear` | `0.35` | Drums a little off their detents, figures a little off register |
|
|
73
|
-
| `light` | `315` | Where the lamp is, in degrees (0 is above, clockwise) |
|
|
74
|
-
| `size` | `'fit'` | `'fit'` fills the host and follows it; a number is the numeral size in px |
|
|
75
|
-
| `label` | `''` | Engraved into the plate under the window |
|
|
76
|
-
| `sound` | `false` | A soft tick per step of the last drum (a whirr when it spins), once the page has had a click or key press |
|
|
77
|
-
| `announce` | `true` | Announce the landed value to screen readers; `false` for an ambient tally |
|
|
78
|
-
|
|
79
|
-
### Methods
|
|
80
|
-
|
|
81
|
-
- `setValue(n, { instant })`: spring to a new value, or jump with `instant: true`
|
|
82
|
-
- `count(rate)`: tally `rate` per second; `count(0)` stops
|
|
83
|
-
- `update(options)`: change any option
|
|
84
|
-
- `destroy()`: stop and remove everything
|
|
85
|
-
- `value`, `state`: the target value, and `running` or `asleep`
|
|
86
|
-
|
|
87
|
-
## How the carry works
|
|
88
|
-
|
|
89
|
-
Only the last drum is driven. Every other drum moves only while the drum to
|
|
90
|
-
its right passes 9 → 0. The first carry takes the last tenth of the units'
|
|
91
|
-
step, and every carry above it turns in lock-step with the drum below, which is
|
|
92
|
-
why the carries all roll together at the end.
|
|
93
|
-
|
|
94
|
-
That rule is one pure function of a single continuous value:
|
|
95
|
-
|
|
96
|
-
```js
|
|
97
|
-
export function drumPositions(value, count, decimals = 0, carry = CARRY) {
|
|
98
|
-
const n = scaled(value, decimals);
|
|
99
|
-
const out = new Array(count);
|
|
100
|
-
if (count < 1) return out;
|
|
101
|
-
let below = n % 10;
|
|
102
|
-
out[0] = below;
|
|
103
|
-
for (let k = 1; k < count; k += 1) {
|
|
104
|
-
const w = k === 1 ? carry : 1;
|
|
105
|
-
const digit = Math.floor(n / 10 ** k) % 10;
|
|
106
|
-
below = digit + clamp((below - 10 + w) / w, 0, 1);
|
|
107
|
-
out[k] = below;
|
|
108
|
-
}
|
|
109
|
-
return out;
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The value itself is a spring, so the whole counter animates one number and
|
|
114
|
-
every drum follows from this. `scaled()` removes float dust, so 10.1 at two
|
|
115
|
-
decimals is 1010, not 1009.9999999999999.
|
|
116
|
-
|
|
117
|
-
The rest:
|
|
118
|
-
|
|
119
|
-
- **The light doesn't turn.** A drum is a cylinder on a horizontal axle, so
|
|
120
|
-
the light on it depends only on height in the window, not on how far it has
|
|
121
|
-
turned. One overlay per drum, computed once from the lamp; the numerals roll
|
|
122
|
-
through it.
|
|
123
|
-
- **Real text.** The faces are DOM text on a ten-sided CSS 3D drum, so the
|
|
124
|
-
figures are crisp and properly foreshortened.
|
|
125
|
-
- **The numerals** are Big Shoulders Bold, bundled, so the drums print the same
|
|
126
|
-
figures everywhere. The font loads `block` and the figures are re-centred on
|
|
127
|
-
it once it lands, so nothing jumps.
|
|
128
|
-
|
|
129
|
-
## Accessibility and cost
|
|
130
|
-
|
|
131
|
-
- The drums are `aria-hidden`. The number is a visually hidden text element,
|
|
132
|
-
announced politely once the value has landed: never every frame.
|
|
133
|
-
- Reduced motion: the value changes instantly, nothing spins, no frame is
|
|
134
|
-
requested.
|
|
135
|
-
- It costs nothing at rest. The loop runs only while a value is travelling,
|
|
136
|
-
sleeps once it lands, sleeps between the steps of a slow tally, and pauses
|
|
137
|
-
off screen and in a hidden tab. A tab that comes back after two seconds
|
|
138
|
-
can't blow up the spring.
|
|
139
|
-
|
|
140
|
-
## Compared
|
|
141
|
-
|
|
142
|
-
| | tactile-odometer | odometer.js (HubSpot) | NumberFlow |
|
|
143
|
-
|---|---|---|---|
|
|
144
|
-
| 1,099 → 1,100 | tens and hundreds carry together at the end of the units' turn | every digit column slides at once, over the same duration | each digit animates on its own |
|
|
145
|
-
| Dependencies | none | none | one small one (`esm-env`), plus React, Vue and Svelte packages |
|
|
146
|
-
| Screen readers | real text, announced once when it lands | not built in | described as accessible |
|
|
147
|
-
| Reduced motion | instant change | not built in | respected by default |
|
|
148
|
-
| Big jumps | fast drums blur at a spin cap, slow ones roll | each column slides through its ribbon | digits transition |
|
|
149
|
-
|
|
150
|
-
The odometer.js column is from its source (`animateSlide` builds one ribbon
|
|
151
|
-
per digit column and slides them together). The NumberFlow column is from its
|
|
152
|
-
docs and npm, and from watching it; its docs don't describe the carry. If
|
|
153
|
-
anything here is wrong, open an issue and I'll fix the table.
|
|
154
|
-
|
|
155
|
-
## Tests
|
|
156
|
-
|
|
157
|
-
```sh
|
|
158
|
-
npm test
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
The pure parts (the carry, the spring, formatting, fitting, the light) are
|
|
162
|
-
tested in Node with `node:test`.
|
|
163
|
-
|
|
164
|
-
## Where it's from
|
|
165
|
-
|
|
166
|
-
It's the free piece of [Tactile UI](https://tactile-ui.me), a library of
|
|
167
|
-
interface objects that work like the hardware they're drawn from: a pin
|
|
168
|
-
screen, a Chladni plate, a detent dial, a cassette. The rest are paid; this
|
|
169
|
-
one is MIT, for everyone.
|
|
170
|
-
|
|
171
|
-
Made by [Ciprian Titire](https://titi.re).
|
|
172
|
-
|
|
173
|
-
## Licence
|
|
174
|
-
|
|
175
|
-
MIT. The numerals, `big-shoulders-bold.woff2`, are under the SIL Open Font
|
|
176
|
-
License 1.1; see `BigShoulders-OFL.txt`.
|
|
1
|
+
# tactile-odometer
|
|
2
|
+
|
|
3
|
+
A mechanical drum counter for the web that carries like the real thing.
|
|
4
|
+
|
|
5
|
+
On a real odometer a drum only moves while the one to its right is carrying,
|
|
6
|
+
so 1,099 → 1,100 rolls the last three drums over together, right at the end of
|
|
7
|
+
the units' turn. Most animated counters move every digit on its own, so
|
|
8
|
+
nothing carries. This one does.
|
|
9
|
+
|
|
10
|
+
Vanilla ES module and CSS, no dependencies, no build step. MIT.
|
|
11
|
+
|
|
12
|
+
**[Play with it →](https://tactile-ui.me/objects/odometer)**
|
|
13
|
+
|
|
14
|
+
<!-- demo gif: 1,099 → 1,100, then +37 -->
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install tactile-odometer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or straight from a CDN:
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tactile-odometer/odometer.css">
|
|
26
|
+
<script type="module">
|
|
27
|
+
import Odometer from 'https://cdn.jsdelivr.net/npm/tactile-odometer/odometer.mjs';
|
|
28
|
+
</script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<div id="seats" style="width: 480px; height: 160px"></div>
|
|
35
|
+
|
|
36
|
+
<script type="module">
|
|
37
|
+
// with a bundler; without one, use the CDN URLs above
|
|
38
|
+
import Odometer from 'tactile-odometer';
|
|
39
|
+
import 'tactile-odometer/odometer.css';
|
|
40
|
+
|
|
41
|
+
const seats = new Odometer(document.getElementById('seats'), { value: 1099 });
|
|
42
|
+
|
|
43
|
+
seats.setValue(1100); // rolls, and carries
|
|
44
|
+
seats.setValue(4821.75); // springs there; fast drums spin, slow ones roll
|
|
45
|
+
seats.count(1); // a live tally, +1 a second (0 stops it)
|
|
46
|
+
seats.update({ finish: 'brass' });
|
|
47
|
+
seats.destroy(); // removes everything it added
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It works in React, Vue, Svelte or anything else that can hand it an element:
|
|
52
|
+
create it in an effect or `onMount`, call `destroy()` on unmount.
|
|
53
|
+
|
|
54
|
+
### Options
|
|
55
|
+
|
|
56
|
+
| Option | Default | What it does |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `value` | `0` | The number to start on |
|
|
59
|
+
| `decimals` | `0` | Drums after the decimal point (0 to 6) |
|
|
60
|
+
| `prefix`, `suffix` | `''` | Printed on the plate either side of the window: `$`, `km`, `seats left` |
|
|
61
|
+
| `locale` | `'en-US'` | Group and decimal marks, grouping pattern and digit glyphs, from `Intl.NumberFormat` |
|
|
62
|
+
| `grouping` | `true` | Fixed plates between drums: 1,099 rather than 1099 |
|
|
63
|
+
| `digits` | `1` | Whole-number drums to reserve, so a growing number doesn't move the layout |
|
|
64
|
+
| `zeros` | `false` | Print 001,099 like a car odometer, instead of blanks |
|
|
65
|
+
| `finish` | `'ink'` | `ink`, `bone`, `steel` or `brass`, each in its own housing |
|
|
66
|
+
| `red` | `'none'` | Signal-red drums: `last`, or all the `decimals` (the classic tenths) |
|
|
67
|
+
| `bezel` | `'slot'` | One `slot` across the drums, `cells` like a tally counter, or `bare` to sit in a line of text |
|
|
68
|
+
| `stiffness` | `140` | How hard the value is pulled to a new number |
|
|
69
|
+
| `damping` | `0.85` | Below 1 it overshoots and winds back; 1 and above it arrives without passing |
|
|
70
|
+
| `maxSpin` | `24` | Digits per second; past this a drum is drawn spinning at the cap, blurred, instead of strobing |
|
|
71
|
+
| `count` | `0` | A live tally per second, in steps of the last drum; negative counts down |
|
|
72
|
+
| `wear` | `0.35` | Drums a little off their detents, figures a little off register |
|
|
73
|
+
| `light` | `315` | Where the lamp is, in degrees (0 is above, clockwise) |
|
|
74
|
+
| `size` | `'fit'` | `'fit'` fills the host and follows it; a number is the numeral size in px |
|
|
75
|
+
| `label` | `''` | Engraved into the plate under the window |
|
|
76
|
+
| `sound` | `false` | A soft tick per step of the last drum (a whirr when it spins), once the page has had a click or key press |
|
|
77
|
+
| `announce` | `true` | Announce the landed value to screen readers; `false` for an ambient tally |
|
|
78
|
+
|
|
79
|
+
### Methods
|
|
80
|
+
|
|
81
|
+
- `setValue(n, { instant })`: spring to a new value, or jump with `instant: true`
|
|
82
|
+
- `count(rate)`: tally `rate` per second; `count(0)` stops
|
|
83
|
+
- `update(options)`: change any option
|
|
84
|
+
- `destroy()`: stop and remove everything
|
|
85
|
+
- `value`, `state`: the target value, and `running` or `asleep`
|
|
86
|
+
|
|
87
|
+
## How the carry works
|
|
88
|
+
|
|
89
|
+
Only the last drum is driven. Every other drum moves only while the drum to
|
|
90
|
+
its right passes 9 → 0. The first carry takes the last tenth of the units'
|
|
91
|
+
step, and every carry above it turns in lock-step with the drum below, which is
|
|
92
|
+
why the carries all roll together at the end.
|
|
93
|
+
|
|
94
|
+
That rule is one pure function of a single continuous value:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
export function drumPositions(value, count, decimals = 0, carry = CARRY) {
|
|
98
|
+
const n = scaled(value, decimals);
|
|
99
|
+
const out = new Array(count);
|
|
100
|
+
if (count < 1) return out;
|
|
101
|
+
let below = n % 10;
|
|
102
|
+
out[0] = below;
|
|
103
|
+
for (let k = 1; k < count; k += 1) {
|
|
104
|
+
const w = k === 1 ? carry : 1;
|
|
105
|
+
const digit = Math.floor(n / 10 ** k) % 10;
|
|
106
|
+
below = digit + clamp((below - 10 + w) / w, 0, 1);
|
|
107
|
+
out[k] = below;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The value itself is a spring, so the whole counter animates one number and
|
|
114
|
+
every drum follows from this. `scaled()` removes float dust, so 10.1 at two
|
|
115
|
+
decimals is 1010, not 1009.9999999999999.
|
|
116
|
+
|
|
117
|
+
The rest:
|
|
118
|
+
|
|
119
|
+
- **The light doesn't turn.** A drum is a cylinder on a horizontal axle, so
|
|
120
|
+
the light on it depends only on height in the window, not on how far it has
|
|
121
|
+
turned. One overlay per drum, computed once from the lamp; the numerals roll
|
|
122
|
+
through it.
|
|
123
|
+
- **Real text.** The faces are DOM text on a ten-sided CSS 3D drum, so the
|
|
124
|
+
figures are crisp and properly foreshortened.
|
|
125
|
+
- **The numerals** are Big Shoulders Bold, bundled, so the drums print the same
|
|
126
|
+
figures everywhere. The font loads `block` and the figures are re-centred on
|
|
127
|
+
it once it lands, so nothing jumps.
|
|
128
|
+
|
|
129
|
+
## Accessibility and cost
|
|
130
|
+
|
|
131
|
+
- The drums are `aria-hidden`. The number is a visually hidden text element,
|
|
132
|
+
announced politely once the value has landed: never every frame.
|
|
133
|
+
- Reduced motion: the value changes instantly, nothing spins, no frame is
|
|
134
|
+
requested.
|
|
135
|
+
- It costs nothing at rest. The loop runs only while a value is travelling,
|
|
136
|
+
sleeps once it lands, sleeps between the steps of a slow tally, and pauses
|
|
137
|
+
off screen and in a hidden tab. A tab that comes back after two seconds
|
|
138
|
+
can't blow up the spring.
|
|
139
|
+
|
|
140
|
+
## Compared
|
|
141
|
+
|
|
142
|
+
| | tactile-odometer | odometer.js (HubSpot) | NumberFlow |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| 1,099 → 1,100 | tens and hundreds carry together at the end of the units' turn | every digit column slides at once, over the same duration | each digit animates on its own |
|
|
145
|
+
| Dependencies | none | none | one small one (`esm-env`), plus React, Vue and Svelte packages |
|
|
146
|
+
| Screen readers | real text, announced once when it lands | not built in | described as accessible |
|
|
147
|
+
| Reduced motion | instant change | not built in | respected by default |
|
|
148
|
+
| Big jumps | fast drums blur at a spin cap, slow ones roll | each column slides through its ribbon | digits transition |
|
|
149
|
+
|
|
150
|
+
The odometer.js column is from its source (`animateSlide` builds one ribbon
|
|
151
|
+
per digit column and slides them together). The NumberFlow column is from its
|
|
152
|
+
docs and npm, and from watching it; its docs don't describe the carry. If
|
|
153
|
+
anything here is wrong, open an issue and I'll fix the table.
|
|
154
|
+
|
|
155
|
+
## Tests
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
npm test
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The pure parts (the carry, the spring, formatting, fitting, the light) are
|
|
162
|
+
tested in Node with `node:test`.
|
|
163
|
+
|
|
164
|
+
## Where it's from
|
|
165
|
+
|
|
166
|
+
It's the free piece of [Tactile UI](https://tactile-ui.me), a library of
|
|
167
|
+
interface objects that work like the hardware they're drawn from: a pin
|
|
168
|
+
screen, a Chladni plate, a detent dial, a cassette. The rest are paid; this
|
|
169
|
+
one is MIT, for everyone.
|
|
170
|
+
|
|
171
|
+
Made by [Ciprian Titire](https://titi.re).
|
|
172
|
+
|
|
173
|
+
## Licence
|
|
174
|
+
|
|
175
|
+
MIT. The numerals, `big-shoulders-bold.woff2`, are under the SIL Open Font
|
|
176
|
+
License 1.1; see `BigShoulders-OFL.txt`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tactile-odometer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A mechanical drum counter for the web that carries like the real thing: 1,099 to 1,100 rolls three drums together. Vanilla JS, no dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./odometer.mjs",
|