Spin lifecycle
A single spin moves through four phases per reel:
START → SPIN → [ANTICIPATION] → STOP → landed
START accelerates from rest with a tiny step-back “pull.” SPIN holds full
speed until the server answer arrives. ANTICIPATION (optional) slows a
specific reel for dramatic tension. STOP decelerates onto the target frame
with a bounce.
The happy path
reelSet.events.on('spin:start', () => console.log('pulled the handle'));
reelSet.events.on('spin:allStarted', () => console.log('all reels at full speed'));
reelSet.events.on('spin:stopping', (i) => console.log('reel', i, 'braking'));
reelSet.events.on('spin:reelLanded', (i, symbols) => console.log('reel', i, 'landed on', symbols));
reelSet.events.on('spin:allLanded', (result) => console.log('final grid', result.symbols));
reelSet.events.on('spin:complete', (result) => console.log('spin done in', result.duration, 'ms'));
await reelSet.spin(); // triggers all of the above
Pattern: fetch result mid-spin
Real slots call the server while the reels are spinning. pixi-reels is built for exactly this.
const promise = reelSet.spin(); // reels accelerate and spin
const response = await fetch('/api/spin').then((r) => r.json());
reelSet.setResult(response.symbols); // ← triggers STOP phase
if (response.anticipationReels?.length) {
reelSet.setAnticipation(response.anticipationReels);
}
const result = await promise;
setResult() must be called while the reels are spinning. If you call it
too early (before spin:allStarted), the engine defers the stop until all
reels are in the SPIN phase.
Pattern: player slam-stop
The library exposes three slam verbs with three different intents:
| Verb | Intent | Side effects |
|---|---|---|
skip() | Round-aware “player tapped the slam button” | First press of the round also boosts speed (standard mode) or auto-slams future refills (cascade mode). Emits skip:boosted when boost applies. |
requestSkip() | ”Slam when ready”. safe BEFORE setResult() arrives | No boost, no auto-slam. Queues until setResult(), then slams once. |
slamStop() | Unconditional “land NOW”. tests / anti-cheat / programmatic | No boost, no auto-slam. |
button.addEventListener('click', () => {
if (reelSet.isSpinning) {
// requestSkip is the safe variant. if the player taps before the
// server response arrives, it queues until setResult() and then
// slams. skip() in pre-result state is a no-op.
reelSet.requestSkip();
} else {
reelSet.spin();
}
});
All three force-land on whatever setResult() told the engine. result.wasSkipped === true. The lifecycle hooks (spin:reelLanded, spin:allLanded, spin:complete) all still fire on the slam path. so win presenters and effect chains keep working without a separate code path.
The skipStage getter reports the round’s stage (0 before any press, 2 after). drive a “SPIN → SKIPPED” button label from it.
Pattern: hold some reels
// Reels 0 and 4 are frozen for this spin. only the middle three reroll.
const spin = reelSet.spin({ holdReels: [0, 4] });
reelSet.setResult(serverGrid); // entries at held indices are ignored
await spin;
Held reels skip START / SPIN / STOP entirely and stay on whatever symbols
they’re currently showing. They count as already-landed for spin:allLanded,
so the resolver fires when all non-held reels land. No spin:reelLanded /
spin:stopping fires for held reels. setAnticipation([...]) filters held
indices silently. See the hold-and-win recipe for
when to use this vs the per-cell pattern.
Pattern: hybrid spin-then-cascade
const reelSet = new ReelSetBuilder()
// ...
.tumble({
fall: { duration: 280, ease: 'power3.in', rowStagger: 60 },
dropIn: { duration: 450, ease: 'power3.out', rowStagger: 60, distance: 'perHole' },
})
.ticker(app.ticker)
.build();
await reelSet.spin(); // round 1. strip-spin (default mode)
await reelSet.spin({ mode: 'cascade' }); // respin. cascade drop-in
SpinOptions.mode overrides the builder’s default phase chain on a
per-call basis. The engine throws if you pick 'cascade' without
.tumble(...) on the builder. the error names the missing method.
See the spin-then-cascade recipe.
Pattern: nudge after landing
After a spin lands, you can shift a single reel by N positions to
reveal caller-supplied symbols via reelSet.nudge(col, ...). The spin
pipeline is idle during a nudge; the nudge’s own tween drives the
strip.
await reelSet.spin();
// landed on a near-miss
await reelSet.nudge(2, { distance: 1, direction: 'down', incoming: ['wild'] });
// reel 2 has now shifted; nudge:complete fired
Multi-reel beats are Promise.all([...]) of independent calls. Cancel
mid-tween via NudgeOptions.signal (rejects with AbortError +
nudge:cancelled), or fast-forward via reelSet.skipNudge(col)
(resolves normally).
Read the full contract in the nudge guide. Live recipes: nudge, skip, abort, stagger, spotlight after a nudge, big symbols.
Full event map
| Event | Payload | When |
|---|---|---|
spin:start | . | Any spin() call |
spin:allStarted | . | Every (non-held) reel is in SPIN phase |
spin:stopping | (reelIndex) | A reel begins STOP (held reels never fire) |
spin:reelLanded | (reelIndex, symbols) | Individual reel landed (held reels never fire) |
spin:allLanded | (result) | Last non-held reel landed |
spin:complete | (result) | Just after spin:allLanded |
skip:requested | . | A slam fired. from skip(), requestSkip() (after setResult), or slamStop() |
skip:completed | . | All non-held reels force-landed |
skip:boosted | ({ previous, current }) | First skip() press of a standard-mode round; engine bumped speed to the fastest registered profile for the rest of the round. Cascade mode auto-slams refills instead. |
speed:changed | (profile, previous) | setSpeed() called |
spotlight:start | (positions) | spotlight.cycle(...) began |
spotlight:end | . | Spotlight finished |
pin:placed | (pin) | reelSet.pin(...) succeeded |
pin:expired | (pin, reason) | Pin removed by unpin, turns exhausted, or 'eval' reset |
pin:moved | (pin, from) | reelSet.movePin(...) resolved |
pin:migrated | (pin, info) | MultiWays reshape moved a pin to a new row |
pin:overlayCreated | (pin, symbol) | Mid-spin overlay symbol mounted for a pin |
pin:overlayDestroyed | (pin, symbol) | Mid-spin overlay torn down on land |
shape:changed | (rowsPerReel) | MultiWays setShape(...) accepted |
adjust:start | ({ reelIndex, fromRows, toRows }) | AdjustPhase entered for a reel |
adjust:complete | ({ reelIndex }) | AdjustPhase finished |
nudge:start | ({ reelIndex, distance, direction }) | reelSet.nudge(...) pre-placement done; tween about to begin |
nudge:complete | ({ reelIndex, distance, direction, symbols }) | Nudged reel has snapped to its new grid position |
nudge:cancelled | ({ reelIndex, distance, direction, reason }) | NudgeOptions.signal aborted or the reel was destroyed mid-tween. Does not fire alongside nudge:complete. |
destroyed | . | destroy() called |
Deeper dive
- Phases. customize START/SPIN/STOP timing
- Events. types and exact signatures
- SpinOptions.
holdReels,mode