pixi-wheels
Building blocks

Spin lifecycle

idle -> starting -> cruising -> stopping -> [settling] -> idle
  ^                    |            ^
  |               setResult()   the plan: decel [+ bait legs] [+ settle legs]
idling (optional slow rotation while nothing happens)

States#

StateWhat the ring doesAllowed calls
idleNothing.spin(), idle.start(), setWeights()
idlingTurns slowly at the idle speed.spin() (ramps from the idle speed), idle.stop()
startingAccelerates to spinSpeed over accelerationMs.setResult(), skip(), requestSkip()
cruisingConstant speed, waiting for the result and the minimum times.setResult(), skip(), requestSkip()
stoppingPlays the planned legs: deceleration, anticipation, landing.skip(), slamStop()
settlingPlays the settle legs (centre glide or bounce).nothing; wait

spin() while spinning throws. setResult() twice, or after the stop began, throws. Read wheel.main.state to branch.

The planned stop#

Once the result is in and the profile’s minimumSpinTime and minCruiseMs have passed, the engine plans the stop as a list of legs and plays them back. The first leg starts at the cruise speed: its ease’s initial slope is matched to the speed, so the distance fixes the duration and the planner picks the number of full turns closest to stopDuration. No velocity step when the result arrives.

  • Plain stop: one leg to the landing angle.
  • Creep: a leg to the bait’s entry edge ending at creepSpeed, then a crawl over the line to a rest just inside the target.
  • Stutter: the same crawl, slowing to a near-stall a hair short of the line, then a slip over it.
  • Stall: a leg to the target’s entry edge ending at creepSpeed, then a crawl toward the bait’s line that dies just short of it.
  • Settle: a dwell and a glide to the centre, or a bounce out and back.

No plan contains a reverse leg or a stop before the rest: the section the wheel stops on is the result. spin:stopping reports the turn count and duration; the debug overlay draws the current leg.

Landing angle#

setResult() resolves the target to a wheel-local angle at once: the section’s middle, a random spot inside it, or the exact offset or angle you passed. A tease moves that angle next to the divider the target shares with the bait (rest). The plan is built against that angle, so nothing after setResult() moves the landing. Changing weights after it can put a different section under the fixed angle; the engine warns (landing-moved) when that happens.

Skip and slam#

skip() replaces the remaining legs with a fast-forward to the landing (at least a third of a turn, over the profile’s skipDuration) and keeps the settle. Pressed before the stop began, it also emits spin:stopping. With protectAnticipation, the first press fast-forwards to the bait and the second lands. requestSkip() holds a press until the result is in and skip.minimumSpinTime has passed. slamStop() snaps to the final position and completes synchronously.

Result#

interface WheelSpinResult {
  ring: string;          // 'main' on a single-ring wheel
  section: ResolvedSection;
  landingAngle: number;  // wheel-local degrees under the pointer
  offset: number;        // 0..1 inside the section
  wasSkipped: boolean;
  duration: number;      // ms from spin() to complete
  turns: number;         // full turns made
}