Odometer Number Counter
- Skia
- Reanimated
- Haptics
A counter whose digits are physical wheels. Each column holds one strip of ten glyphs, clipped to a single cell and slid vertically, so the number changes by moving type past a window rather than by swapping characters.
Each column is a ten-glyph strip
The glyphs 0–9 are laid out once at i * size apart and drawn as a single Glyphs node. The column then translates by the wheel offset times the cell height inside a clipped group, so only one digit is ever visible. Nothing is re-measured or re-created as the value changes.
return ids.map((id, i) => ({ id, pos: { x: (glyphWidth - (advances[i] ?? glyphWidth)) / 2, y: i * size + baseline },}));// …const transform = useDerivedValue(() => [{ translateY: -offset.get() * size }]);Only the column about to turn moves
Each wheel works out its own digit and the digit it would show if the number were one higher. If they match it sits still; if they differ the fractional part of the value, smoothed with f * f * (3 - 2f), carries it between the two. That is why the units wheel spins continuously while the thousands wheel waits and turns once.
const dNow = ((Math.floor(n / p10) % 10) + 10) % 10;const dNext = ((Math.floor((n + 1) / p10) % 10) + 10) % 10;const e = f * f * (3 - 2 * f);const off = dNow === dNext ? dNow : dNow + e;Blur measured from the wheel's own speed
Each column tracks how far it moved since the last frame and divides by the elapsed time, then feeds that speed to a BlurMask. Fast wheels smear, slow ones stay crisp, with no per-digit configuration. The delta is wrapped across the 9-to-0 seam first, or one frame a second would report a ten-digit jump and blur everything.
let d = off - prevCont.get();if (d > 5) d -= 10;else if (d < -5) d += 10;velocity.set(dt > 0 ? d / dt : 0);// …const blur = useDerivedValue(() => rollBlur(velocity.get()));Tuning
- 1.6
- On
<OdometerCounter params={{ duration: 1.6, grouping: true }} fireSignal={0} />