pixi-reels
Building blocks

Symbols

A symbol is what fills one cell. Three ship with the library. The fourth is for everything else.

ClassUse it for
SpriteSymbolStatic images. One texture per id.
AnimatedSpriteSymbolSprite-sheet animation loops.
SpineSymbolSkeletal animation via @esotericsoftware/spine-pixi-v8.
ReelSymbol (abstract)Roll your own.

Register#

symbols() hands you a registry. You register a class, not an instance.

You never call new on a symbol. The engine does, and it pools them.

builder.symbols((r) => {
  r.register('cherry', SpriteSymbol, { textures: { cherry: cherryTex } });
  r.register('bar',    AnimatedSpriteSymbol, { frames: barFrames, animationSpeed: 1.5 });
});

Custom symbol#

Extend ReelSymbol. Lifecycle: activate -> maybe playWin / stopAnimation -> deactivate.

resize(width, height) runs on every swap. Put positioning there, not in the constructor, or your symbols scatter.

import { ReelSymbol } from 'pixi-reels';
import { Graphics } from 'pixi.js';

export class BlockSymbol extends ReelSymbol {
  private _g = new Graphics();

  constructor(opts: { colors: Record<string, number> }) {
    super();
    this.view.addChild(this._g);
    (this as any)._opts = opts;
  }

  protected onActivate(symbolId: string): void {
    const color = (this as any)._opts.colors[symbolId] ?? 0x888888;
    this._g.clear().roundRect(0, 0, 140, 140, 10).fill(color);
  }

  protected onDeactivate(): void {
    this._g.clear();
  }

  async playWin(): Promise<void> {
    // pulse for 400ms
    this.view.scale.set(1.1);
    await new Promise((r) => setTimeout(r, 400));
    this.view.scale.set(1);
  }

  stopAnimation(): void { this.view.scale.set(1); }
  resize(w: number, h: number): void { this._g.clear().roundRect(0, 0, w, h, 10).fill(0x333333); }
}

// Register:
builder.symbols((r) => {
  r.register('red',   BlockSymbol, { colors: { red: 0xff6d70 } });
  r.register('blue',  BlockSymbol, { colors: { blue: 0x4cc2ff } });
});

Headless testing#

HeadlessSymbol draws nothing. No textures to load, no canvas needed. Use it in tests.

import { HeadlessSymbol } from 'pixi-reels/testing';

builder.symbols((r) => {
  r.register('a', HeadlessSymbol, {});
  r.register('b', HeadlessSymbol, {});
});

See Cheats & testing for the full recipe.

Weights#

No weight given? You get 10. Change it with builder.weights({ id: n }).

builder.weights({ cherry: 40, bar: 20, seven: 4 });

Bigger number, more often. Weights only touch RANDOM fill — the strip while it spins, and the buffer cells.

A grid from setResult() ignores weights completely. The server decides what lands. Weights decide what blurs past on the way there.

weights() is one table for the whole set. To vary it — ban a symbol from the buffer cells, or make one reel draw differently — layer a pool on top:

builder.randomSymbols({ exclude: ['EMPTY'] });                     // every reel
builder.randomSymbols({ exclude: ['COIN'] }, { slots: 'buffer' }); // buffers only
builder.randomSymbols({ weights: { WILD: 40 } }, { reel: 2 });     // reel 2 only

The same pools are reachable at run time as reelSet.randomSymbols.set(...), which is where a game-mode switch belongs. See Buffer indexing.