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#
| Method | Default |
|---|---|
.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#
| Field | Type | Default | Notes |
|---|---|---|---|
duration | number (ms) | 300 | Each symbol’s fall-out tween length. |
ease | GSAP easing string | 'sine.in' | Gravity feel. Any GSAP easing works. |
cellStagger | number (ms) | 0 | Delay 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#
| Field | Type | Default | Notes |
|---|---|---|---|
duration | number (ms) | 600 | Each symbol’s drop-in tween length. |
ease | GSAP 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. |
cellStagger | number (ms) | 60 | Delay 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.
| Feel | fall.ease | dropIn.ease | dropIn.cellStagger | When to use |
|---|---|---|---|---|
| Classic | sine.in | back.out(1.6) | 50 ms | Default. Start here. |
| Cartoon bounce | sine.in | bounce.out | 70 ms | Multi-bounce landing. |
| Slam | power4.in | expo.out | 25 ms | Turbo mode. sub-half-second tumbles. |
| Rain column | sine.in | sine.in | 0 ms (+ distance: 'auto') | Match-3 / puzzle / chess-board grids. |
| Wave | sine.in | back.out(2.0) | 110 ms | Strong 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.
| Field | Type | Notes |
|---|---|---|
holdReels | number[] | 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.