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.
Files changed (2) hide show
  1. package/README.md +176 -176
  2. 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.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",