Symbol building blocks and how they are drawn: the classes you register, the three ways to layer overflowing art, and spinning on cached blur textures.
CardSymbol. debug / prototyping helper#
No-asset PIXI.Graphics symbol for recipes, mechanic tests, and prototypes; crisp at any cell size.
Exported from the package, so a prototype needs no art at all:
import { CardSymbol, CARD_DECK, WILD_CARD } from 'pixi-reels'.
Gotcha: debug scaffolding, not production art - ship SpriteSymbol / AnimatedSpriteSymbol / SpineSymbol instead.
Loading recipe…
PhaseCardSymbol: read the phase off the board#
A CardSymbol that is grey at rest and takes the colour of the phase its reel is running - sky for start, blue for spin, amber for anticipation, violet for stop, a green beat on landing. Register it like any symbol, then point one watcher at the reels: PhaseCardSymbol.watch(reelSet.reels) paints every card from the reels’ own phase:enter, landed and symbol:created events, custom phases included, and returns the release.
Gotcha: the card does not know its reel, so without the watcher it stays grey. setPhase() is public for anything else that knows the phase. colors overrides or extends PHASE_CARD_COLORS; a phase with no entry draws other.
Loading recipe…
EmptySymbol: render nothing for a registered id#
Register EmptySymbol against an id to reserve a grid slot that occupies a strip slot but draws zero pixels - the canonical blank field for hold-and-win boards.
Loading recipe…
Texture atlas symbols#
Load sprite symbols from a TexturePacker atlas: one atlas file, 80+ frames, one GPU upload.
Loading recipe…
Authoring a symbol for a sideways set#
A hand-written ReelSymbol on a horizontal set, with wide non-square cells. Nothing in the class knows about the axis: resize(width, height) hands it screen pixels across and screen pixels down whichever way the strip travels, and the plate’s upright marker and glyph stay upright while the strip scrolls along X.
Travel changes motion; facing changes art; they never change each other. The engine never rotates a symbol container to fake an axis, so third-party symbol classes need no changes at all.
Gotcha: the one orientation fact a symbol gets is this.mainAxis ('y' vertical, 'x' horizontal), and it is there for effects that genuinely follow travel, motion blur being the one in the box. Do not lay out with it.
Loading recipe…
The same symbols on both axes#
Four reels of three cells, built twice and landed with the identical grid. One symbol registration serves both sets; the only difference between the two builds is that symbolSize swaps its two numbers, because a horizontal board is the vertical one transposed.
Loading recipe…
Symbol transform#
A symbol morphs into a different (usually higher) one mid-round via setSymbolAt.
Gotcha: transform only after landing (spin:complete / spin:reelLanded); setSymbolAt throws mid-spin and on big-symbol anchors.
Loading recipe…
Reveal a mystery symbol: out, swap, in#
setSymbolAt swaps an identity instantly, so a reveal meant hand-rolling the ordering, the per-cell stagger, a zIndex bump so an overshooting entrance is not clipped, a re-hide after the swap (re-activation resets alpha to 1, so the new art pops for a frame), and abort handling.
await reelSet.swapSymbols(
[0, 1, 2].map((cell) => ({ reel: 1, cell, id: 'WILD' })),
{ outDelay: (_, i) => i * 0.06, holdMs: 200, inDelay: (_, i) => i * 0.09 },
);
The three beats are separately skippable: skipOut for art that plays its own Spine out track, skipIn to leave the cells hidden and reveal them yourself, onSwapped for a midpoint beat that has to await a real animation. playIn() / playOut() are the per-symbol hooks underneath, on the same contract as playDestroy - override them for art-appropriate entrances and exits.
Gotcha: cells are validated up front, and an abort still performs the swap so the board never disagrees with the result. Only valid at rest, and single-cell symbols only - a big symbol spans cells the frame layer reserves, so revealing one is a setResult / setShape job.
Loading recipe…
Which symbols may spawn where#
weights() is one table for the whole set. A pool layers on top of it for a scope: one reel, the buffer cells, or one reel’s buffer cells. It governs the RANDOM draw only - an explicit setResult target always wins.
Keep a symbol out of the hidden cells (globally, or on one reel):
Each spin steps the pool through off, { reel: 1, slots: 'buffer' }, and { slots: 'buffer' }. The labels above and below the grid read the hidden cells live, so you can see COIN stop parking there - on the middle reel first, then everywhere.
Loading recipe…
Give one reel its own draw table:
Watch the spin rather than the landing: reel 0 draws low cards only, reel 2 is a wild reel while it scrolls, reel 4 never teases a wild. Same weights() underneath all five.
Loading recipe…
Symbol layering - zIndex, unmask, promote#
Three tiers of “draw my symbol on top”: within one reel, above every reel and the mask, and per-instance via the spotlight layer.
Within a reel (symbolData zIndex):
Loading recipe…
Cross-reel and out-of-mask (unmask, at rest):
Loading recipe…
Your own order (symbolZIndex): the engine’s formula is type-dominant; a grading spec is row-dominant. Hand the engine a resolver and it keeps applying it after every wrap, swap and landing - here every mid and high is unmasked, so at rest the lower row fronts the one above whatever the symbol, and only a higher grade wins over that.
Loading recipe…
Gotcha: unmask lifts VISIBLE cells only. The same id in a buffer cell stays under the mask, so oversized art cannot hang outside the grid from a slot the player was never meant to see. The demo below lands both at once - and holds through a skip, which is the moment that used to leak.
Loading recipe…
Per-instance (promote the landed view into spotlightContainer):
Loading recipe…
The same job in one call (reelSet.promote): the demo above hand-rolls it - capture each view’s parent and position, reparent into spotlightContainer, put every one back before the strip moves. promote(positions) is that, as a value: it attaches the views to a render layer instead of reparenting, so nothing moves, nothing has to be put back, and the returned release drops them. Unlike spotlight.show() there is no dim, no playWin() and nothing to await, so the game’s own timeline stays in charge. Below, the same tween runs twice - once clipped, once promoted.
Loading recipe…
Landing: the frame, and the beat#
spin:reelLanding fires the frame a reel is on its result - strip snapped, every symbol told it landed, the bounce about to start. spin:reelLanded fires once the bounce has settled, a whole bounceDuration later. Anything coupled to the landing itself belongs on the first one; the demo grows a takeover plate over the copy reels on it and prints the gap.
Loading recipe…
Each symbol hears the same moment through onReelLanded(ctx), with the reel and cell it landed in. A Spine symbol’s autoPlayLanding can be a rule of that context: true for its landing, false for nothing, or an animation name for a different beat - so the copy reels land without a pose to fight the takeover, and the last two reels use a reveal as their landing.
Loading recipe…
Multi-skin Spine tiers on a compact grid#
One skeleton per symbol tier (a skin per id) with static-spin blur, on a gapless 3-4-4-4-4-3 grid.
Loading recipe…
Auto-baked motion blur (static spin)#
Spin cached snapshot textures with a baked blur - no blur art, no runtime filters.
Loading recipe…
Bring your own blur textures#
Hand-authored blur strips where you have them; auto-baked blur where you don’t - both spin through the same StaticSpinSymbol.
Loading recipe…
Spin static, not Spine#
Live Spine skeletons at rest; cached snapshot textures while the reels spin, so no skeleton ticks mid-spin.
Loading recipe…