pixi-wheels
Building blocks

Sections and arcs

A ring is a list of sections laid out clockwise from startAngle (default -90, twelve o’clock). Each section’s arc is its weight share of 360 degrees.

.startAngle(-90)
.sections([
  { id: 'mini',  label: 'MINI',  value: 'mini', weight: 5 },
  { id: 'grand', label: 'GRAND', value: 'grand', weight: 0.5 },
])

Ids, labels, values#

  • id names the section in setResult({ section }), in events and in the debug output. Unique per ring.
  • label is what a label-drawing skin writes. Defaults to the id; '' draws nothing.
  • value is the payload the section stands for. Several sections may share one, and setResult({ value }) lands on one of them.
  • tags are yours. Skins on this site use them to pick authored plates.

Weights#

Relative, positive, finite. weight: 0.5 is half a default section. Change them at run time with wheel.setWeights(); see Dynamic sections.

Styles#

Per-section overrides for the painted skin and for labels on any skin:

style: {
  fill: 0xf1c40f, alpha: 1, stroke: 0xffffff,
  labelColor: 0x2b1d00, labelSize: 30, labelFont: 'Roboto Condensed', labelWeight: '800',
  labelOrientation: 'radial' | 'tangential' | 'upright',
  labelRadius: 0.68,   // fraction of the outer radius
}

Sections without a fill cycle the ring’s palette (.palette([...])).

Reading the geometry#

const s = wheel.geometry.byId('grand');      // { startAngle, endAngle, midAngle, arc, ... }
wheel.geometry.sectionAt(localAngle);        // which section covers a wheel-local angle
wheel.sectionUnderPointer();                 // right now
wheel.main.localAngleUnderPointer();         // wheel-local degrees under the pointer

Angles are wheel-local: they turn with the disc. A point at local angle a is on screen at a + rotation, so the pointer at screen angle p reads local p - rotation. A clockwise spin therefore sweeps decreasing local angles under the pointer: it enters a section through its endAngle and leaves through its startAngle. geometry.entryAngle(section, direction) spells this out; the anticipation planner relies on it.

Rich labels#

A label is not only a string. Give a section content: any container, or a factory that builds one. The label layer of every label-drawing skin (GraphicsRingSkin, TextureRingSkin with labels: true, SpineRingSkin with labels: true, the studio skins on this site) places it at the label radius, rotates it by labelOrientation and fits it into the section’s room.

import { Sprite, Text, Container } from 'pixi.js';

.sections([
  // A ready-made object.
  { id: 'x2', value: 2, content: new Sprite(coinTexture) },
  // A factory: built once, from the section and the room it has.
  { id: 'jackpot', weight: 2, content: (ctx) => {
      const view = new Container();
      const icon = new Sprite(crownTexture);
      const text = new Text({ text: 'JACKPOT', style: { fontSize: 40, fontWeight: '800', fill: 0xffd23f } });
      text.anchor.set(0.5); text.position.y = icon.height / 2 + 24;
      view.addChild(icon, text);
      ctx.fit(view, { padding: 0.1 });   // scale into the slot, 10% free around
      return view;
    },
    style: { labelOrientation: 'tangential', labelRadius: 0.62 } },
  // A Spine instance works the same way: it keeps animating after placement.
  { id: 'bonus', content: () => Spine.from({ skeleton: 'bonusSkeleton', atlas: 'fxAtlas' }) },
])

The factory receives a LabelContext: the resolved section, outerRadius, innerRadius, the slot (see below) and a bound fit(). Content is built once per section and rebuilt only when the section is given a different content. toConfig() keeps the text label and drops content; rich labels are code.

Orientation applies to content as to text. 'tangential' puts the top of the content toward the rim (upright at twelve o’clock); 'tangential-in' toward the hub (upright at six o’clock, for a wheel read from below, like the Pragmatic recipe); 'upright' counter-rotates every frame.

Fitting content#

The room a section offers is a slot: the chord across the wedge at the label radius, and the radial room around that point inside the ring band. labelSlot() computes it; the label layer fits every label into it with labelFit ('contain' by default, also 'cover', 'width', 'height', 'none').

import { labelSlot, scaleToFit, fitContainer, fitText } from 'pixi-wheels';

const slot = labelSlot(section, outerRadius, innerRadius, { radius: 0.68, orientation: 'radial' });
// slot.width / slot.height: the box, oriented like the label. slot.chord, slot.radial: the raw room.

fitContainer(view, slot);                       // sets view.scale so its local bounds fit; returns the factor
fitContainer(view, slot, { mode: 'width', max: Infinity }); // match the width, allowed to grow
fitText(text, { width: slot.width * 0.8, height: slot.height * 0.8 }); // lowers fontSize instead: crisp glyphs
scaleToFit({ width: 400, height: 80 }, slot);   // the pure number, no PixiJS

Content is re-fitted whenever the geometry changes, so dynamic sections shrink and grow their labels with their arcs. Scaling never enlarges by default (max: 1): author content at the size it should have on a full-size wedge and let the fit only take it down.