Skip to content

fix(NitroNumber): a roll keeps up with a value that keeps changing (#62) - #63

Merged
ronickg merged 4 commits into
mainfrom
fix/spring-retarget-62
Oct 5, 2026
Merged

ronickg merged 4 commits into
mainfrom
fix/spring-retarget-62

Conversation

@ronickg

@ronickg ronickg commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Fixes #62. Replaces #61.

What was wrong

On a slider or dial (a new value every frame), a roll figure fell apart in two ways:

  1. easing="spring" stalled. Each new value restarted the spring's step response, which starts at zero speed. The figure froze while the finger moved: $11 for a whole fast drag on iOS, $5 on Android.
  2. Wheels went the long way round. A new value mid-roll re-aimed each wheel from where it stood, in the direction the value moved. A wheel that lagged behind or ran past its digit went a whole turn round, or the short way backwards. fix(NitroNumber): rolls keep up with a fast stream of values (spring speed, drag tracking, direction) #61 kept the spring's speed but still had this, so it overshot ("299" for a real 220) and looked wrong on a real drag.

The first commit here replaced the per-digit roll with a counter (one number counting toward the value). That fixed the stall but rolled through every value in between, so after a fast drag it kept rolling for 0.7–1 s. The second commit puts back per-digit rolling and fixes the two problems above. That's how the numeric transition behaves, and how the other rolling-number libraries work (NumberFlow, number-animation, ticker, rolling-numbers: each digit column rolls on its own).

What

  • Each digit still rolls on its own, from where it is to its new digit, over duration.
  • A new value mid-roll moves where a wheel is going, not where it stands. The new destination is the old destination plus the step from that digit to the new one, in the value's direction. This is NumberFlow's accumulating composite. A one-way stream piles up, so the wheel spins faster to keep up; a drag that turns back takes it off, so the wheel slows and turns. No wheel goes a whole turn round.
  • On the spring, a wheel keeps its speed through a new value. This is NitroNumber: a spring roll that is retargeted mid-roll restarts from zero speed (stalls under rapid value changes) #62's response with an initial velocity, normalised to the new travel (v_rel = v_abs · D / travel). v₀ = 0 reproduces today's curve exactly. The other easings keep their ease-out re-target curve.
  • A single change from rest rolls exactly as before. The numeric and scramble transitions are untouched.
  • C++ only. No API change. The docs' WebAssembly engine is rebuilt.
  • Example: the savings dial runs on the library's default timing, with a transition switch (roll / numeric / scramble). Swipe-back is off on that screen because it was taking fast drags.

Tests

  • bun run test:cpp and test:cpp:sanitize. New engine tests:
    • springKeepsUpWithAStream (NitroNumber: a spring roll that is retargeted mid-roll restarts from zero speed (stalls under rapid value changes) #62's +1/frame stream): the spring covers at least 80% of what easeInOut covers. Fails on main, where the spring barely moves.
    • aNewValueKeepsTheSpeed: the per-frame step just after a new value is ≥ 90% of the step just before. Fails on main (~30%).
    • aSingleSpringRollIsUnchanged: the exact spring positions.
    • aStreamSettlesOnTheLastValue: lands within one duration of the last value, frames stop.
    • aDialSwungLeftAndRightLandsAtOnce: the values a real drag sent; spring and easeInOut, auto and shortest, 500 and 900 ms. No wheel whips round within a frame, and it lands on the last value within one duration.
  • bun run test 171/171, bun run typecheck.
  • iPhone 17 simulator, a real touch drag swung left and right across the dial at the library defaults, recorded. The figure stops moving 450 ms after the finger does, with roll; numeric takes 700 ms (its spring rings out). Every digit lands on the true value.
  • Pixel 9 emulator, the same kind of drag: lands on the true value 500 ms after the finger stops.
  • Physical devices (not used).

Both this and #60 open a ## 0.3.8 changelog section, so whichever merges second needs the two joined.

🤖 Generated with Claude Code

Ronald Goedeke and others added 4 commits October 5, 2026 14:58
The dial's yearly figure as Uno's AmountText sets up NitroNumber (centred and
fitted to the full width, a 900 ms spring roll, proportional digits), on Uno's
numbers: a quadratic dollar track, typeable rounding, 15% a year, nothing under
a 10-dollar minimum. Buttons replay a drag to and from zero for recordings, and
switch between a dollar user ($2) and a peso user (2 USD).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A value arriving while the figure was still rolling started a roll of its
own, wheel by wheel, from where the wheels stood. With easing="spring" each
roll started from rest, so a figure fed by a slider stood still until the
finger stopped; with any easing each wheel chased its own digit of every
value, and a drag swung left and right showed numbers it never passed.

A value that arrives mid-roll is now followed: the figure is one number
counting towards it, as an odometer does, on a damped spring that keeps its
position and speed through every new value (x(t) with an initial velocity:
the spring easing's own stiffness and bounce, critically damped for the
other easings), solved in closed form from when each value arrived. The
wheels are drawn from that number with setValue's carry layout. A roll
caught part way hands over in 150 ms: each wheel is the number's plus the
offset it had, fading out, so it neither jumps nor turns back.

A single change from rest rolls as before (unchanged spring positions are
tested), and the numeric transition, a stagger, a clock's wheels and a
format change in progress keep the wheel-by-wheel roll.

The example's savings dial turns off swipe-back (it took fast drags) and
gains a 900/400 ms spring toggle. The docs' WebAssembly engine is rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsition switch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#62)

The follow (one number counting towards the value) rolled through every
value in between, so after a fast drag it kept rolling long after the finger
stopped. Back to rolling each digit on its own, as the numeric transition and
the other rolling-number libraries do (NumberFlow, number-animation, ticker),
with the two things that broke it on a slider fixed:

- A new value mid-roll moves where each wheel is going, by the step from the
  digit it was heading for (NumberFlow's accumulating composite), rather than
  re-aiming it from where it stands the way the value moved, which sent a
  wheel that lagged or ran past its digit a whole turn round or backwards.
- On the spring, a wheel keeps its speed through a new value (#62's x(t)
  with an initial velocity); the step response from rest stalled it.

Every digit lands within one duration of the last value: 450 ms after the
finger stops on the dial at the defaults (the numeric transition: 700 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ronickg ronickg changed the title fix(NitroNumber): follow a value that keeps changing (#62) fix(NitroNumber): a roll keeps up with a value that keeps changing (#62) Oct 5, 2026
@ronickg
ronickg merged commit 2629735 into main Oct 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NitroNumber: a spring roll that is retargeted mid-roll restarts from zero speed (stalls under rapid value changes)

1 participant