pixi-reels
API

ReelSetBuilder

Every method returns this so you can chain. build() validates and returns a ReelSet. missing required calls throw with a single error listing every problem.

Required#

Method
.reels(n)Number of columns.
.visibleCells(n) or .visibleCellsPerReel([n,...]) or .multiways({...})One of these picks the row layout.
.symbolSize(w, h)Per-symbol dimensions in px.
.symbols((r) => { r.register(...) })At least one symbol id.
.ticker(pixiTicker)Drives frame updates.

Missing any of these throws at .build() with a human-readable list of problems.

Optional#

MethodDefault
.symbolGap(x, y){ x: 0, y: 0 }
.bufferSymbols(n) or .bufferSymbols({ start, end })1 (clamped to ≥ 1. the motion layer needs one buffer cell each side for wrap detection). start is the smaller main coordinate (above for vertical, left for horizontal), whichever way the reel travels.
.weights({ id: n })10 for each registered symbol
.randomSymbols(pool, scope?)No pools. Narrows the RANDOM draw on top of weights(). pool is { weights?, exclude? }; scope is { reel?, slots?: 'spinning' | 'buffer' | 'bufferStart' | 'bufferEnd' } (default: every reel, every random cell). Each layer stacks on the wider ones, so a side pool narrows what 'buffer' already allows. Same pools at run time: reelSet.randomSymbols.
.symbolData({ id: { zIndex, unmask, size, weight } })Per-symbol metadata. Auto-routes unmask; size: { reels, cells } beyond 1x1 declares a big symbol. reels spans the cross axis, cells the main axis, in every orientation.
.reelExtents([n,...])Per-reel MAIN-axis extents for pyramid layouts (pixel height when vertical, width when horizontal). Derived from the cell count if unset.
.reelAnchor('start' | 'center' | 'end')'center'. alignment of short reels along the MAIN axis, inside the longest reel’s extent.
.speed(name, profile)SpeedPresets.NORMAL under 'normal'
.initialSpeed(name)'normal'
.offsetConfig(config){ mode: 'none' }
.spinningMode(mode)new StandardMode()
.frameMiddleware(mw)(none). add as many as you want; sorted by priority.
.phases((f) => { f.register(...) })Built-in START/SPIN/STOP/ANTICIPATION (+ cascade phases when .tumble() is used). f.register(key, Class, options) re-registers a built-in with options; { steps } edits its step list, see Phases.
.skipMode('slam' | 'quicken')'slam'. What a skip press does to the reels it frees when requestSkip() / skipSpin() do not say. See ReelSet.
.tumble({ fall, dropIn })(unset; required for spin({ mode: 'cascade' }) and reelSet.refill(...)). see the Tumble section below.
.multiways({ minCells, maxCells, reelExtent })(unset; required for setShape(...)). reelExtent is a MAIN-axis extent.
.orientation('vertical' | 'horizontal')'vertical'. which screen axis the strip travels along. See Orientation & direction.
.direction('forward' | 'reverse')'forward'. which way along that axis symbols move. 'reverse' on a vertical set is a roll-up.
.directionPerReel([d,...])(unset). per-reel override; length must equal reels(). Cannot be combined with cross-reel big symbols.
.cellStacking('ascending' | 'descending')'ascending'. which cell draws in front. Geometric: direction('reverse') does NOT flip it.
.reelStacking('ascending' | 'descending')'ascending'. which reel draws in front.
.pinMigrationDuration(ms | fn)200 (MultiWays only)
.pinMigrationEase(ease)'power2.out' (MultiWays only)
.maskStrategy(strategy)Per-reel RectMaskStrategy. Auto-switches to SharedRectMaskStrategy when big or unmasked symbols are registered and the CROSS-axis gap is > 0. A custom strategy takes one MaskContext and must declare version = MASK_STRATEGY_VERSION.
.gsap(instance)The package’s own gsap import. Use this when your app and the engine resolve to different gsap instances (symlinked workspaces, npm link, misconfigured dedupe). Bound PER SET at build(); pass the same instance to driveGsapWithTicker(ticker, instance).
.initialFrame(ColumnTarget[])Random per weights(). One ColumnTarget per reel: { visible, bufferStart?, bufferEnd? }.
.rng(fn)Math.random. Source for every symbol the engine picks itself — the strip you watch scroll, and any buffer padding. Must return [0, 1). Server-authoritative outcomes do not make that visible sequence reproducible; pass a seeded, audited PRNG if you need to replay a spin from its seed.
.poolCapacity(n)The whole strip (visible + buffer), per symbol id, so a grid that briefly goes all-one-symbol still recycles. Lower it to cap memory on very large grids; raise it for unusually heavy simultaneous swaps.

.build()#

Validates every required field, instantiates every subsystem, returns a ReelSet. Throws synchronously with the full list of problems. you never see a half-built reel set.

Minimal example#

import { ReelSetBuilder, SpriteSymbol, SpeedPresets } from 'pixi-reels';

const reelSet = new ReelSetBuilder()
  .reels(5)
  .visibleCells(3)
  .symbolSize(140, 140)
  .symbolGap(4, 4)
  .symbols((r) => {
    r.register('cherry', SpriteSymbol, { textures: { cherry: cherryTex } });
  })
  .weights({ cherry: 40 })
  .speed('turbo', SpeedPresets.TURBO)
  .ticker(app.ticker)
  .build();

Tumble#

.tumble(config?) enables cascade mechanics. It replaces the strip-spin + bounce-stop chain with a three-phase pipeline (cascade:fall · cascade:place · cascade:dropIn) and flips the default spin mode to 'cascade' for that builder.

.tumble({
  fall:   { duration: 280, ease: 'sine.in',       cellStagger: 40 },
  dropIn: { duration: 480, ease: 'back.out(1.6)', cellStagger: 50, distance: 'perHole' },
})

TumbleFallConfig#

FieldTypeDefaultNotes
durationnumber (ms)300Each symbol’s fall-out tween length.
easeGSAP easing string'sine.in'Gravity feel. Any GSAP easing works.
cellStaggernumber (ms)0Delay between successive rows starting their fall. 0 = every row falls together.
cellOrder'auto' | 'endFirst' | 'startFirst''auto'Which row begins first. 'auto' starts at the gravity-exit end, so the default pairs with setDropOrder('ltr') for the canonical “bottom-left first, top-right last” feel, and inverts by itself when gravity does. 'endFirst'/'startFirst' pin a geometric end.

TumbleDropInConfig#

FieldTypeDefaultNotes
durationnumber (ms)600Each symbol’s drop-in tween length.
easeGSAP easing string'power2.out'Clean deceleration with no overshoot. matches commercial slots that play a landing spine animation after the drop. Use 'back.out(1.5)' for soft overshoot, 'bounce.out' for cartoon bounce, 'expo.out' for slam.
cellStaggernumber (ms)60Delay between successive rows starting their drop. 0 = simultaneous (the canonical refill choice).
cellOrder'auto' | 'endFirst' | 'startFirst''auto'Which row arrives first when cellStagger > 0. 'auto' fills from the gravity-exit end, the way a stack settles from its floor up.
distance'perHole' | 'auto' | number'perHole''perHole' is gravity-correct (each symbol falls exactly its own offset). 'auto' makes every symbol traverse the full column in unison (“rain column” feel). A number forces a uniform pixel distance.

Picking a feel#

Five preset recipes you can copy verbatim. see the tumble feels recipe for side-by-side canvases.

Feelfall.easedropIn.easedropIn.cellStaggerWhen to use
Classicsine.inback.out(1.6)50 msDefault. Start here.
Cartoon bouncesine.inbounce.out70 msMulti-bounce landing.
Slampower4.inexpo.out25 msTurbo mode. sub-half-second tumbles.
Rain columnsine.insine.in0 ms (+ distance: 'auto')Match-3 / puzzle / chess-board grids.
Wavesine.inback.out(2.0)110 msStrong per-row stagger. row-by-row arrival.

Overriding a phase#

.tumble() registers three named phases (cascade:fall, cascade:place, cascade:dropIn). Each is an independently overridable ReelPhase subclass:

builder
  .tumble({ /* ... */ })
  .phases((f) => f.register('cascade:fall', MyCometFallPhase));

See docs/recipes/tumble-cascade.md for the phase contract and an override example.

Spin options#

reelSet.spin(options?) accepts SpinOptions. All fields are optional.

FieldTypeNotes
holdReelsnumber[]Reel indices to FREEZE for this spin. Held reels skip START/SPIN/STOP entirely and stay on their current symbols. They count as already-landed for spin:allLanded. Out-of-range / duplicate / non-integer entries are silently filtered. setAnticipation([...]) and setStopDelays([...]) filter held indices too.
mode'standard' | 'cascade'Phase-chain selector for this spin. Overrides the builder default. 'cascade' requires .tumble(...) on the builder. the engine throws at spin() if cascade phases aren’t registered.
// Hold reels 0 and 4; only the middle three reroll.
const spin = reelSet.spin({ holdReels: [0, 4] });
reelSet.setResult(serverGrid); // entries at 0/4 are ignored
await spin;

// Per-spin cascade override on a builder that registered both modes.
await reelSet.spin({ mode: 'cascade' });

See the spin lifecycle guide for how these options interact with the event timeline, and the cascades guide for the cascade-specific orchestration.