The tongue, the flapper, the stopper, the clapper, the ticker: the fixed thing the wheel is read against, and the only part of a bonus wheel that is allowed to be floppy. Get it right and a spin sounds and feels mechanical. Get it wrong and the wheel looks like a spinning picture.
This recipe is all of it. The disc it rides on has its own recipe.
Anatomy#
.pegs({ size: 6 }) // studs on the disc, one per divider,
// set just inside the tongue's tip
.pointer({
id: 'top', // 'pointer' for the first one
angle: -90, // -90 twelve o'clock, 0 three, 90 six
facing: 'inward', // on the rim, tip toward the hub
tipInset: 18, // how far the tip reaches past the rim
skin: { type: 'graphics', shape: 'tongue', length: 72, width: 36, pinRadius: 8 },
flap: { elasticity: 1, friction: 0.35, stiffness: 420, damping: 14, maxAngle: 28, tipWidth: 14 },
})
Results are read against the first pointer. Add more for decoration or a second read position; each one ticks on its own.
Eight sections, eight fat pegs, one long tongue, idling at 24 deg/s so every stage of the contact is visible:
Loading recipe…
The pegs#
Pegs are the wheel’s half of the contact: small circles on the disc that the tip has to climb over. Every ring has them by default, one per divider, so a tick always lands on a line.
.pegs({ size: 6, inset: 9 }) // the default
.pegs({ size: 9, inset: 14, angles: [0, 45, 90, 135, 180, 225, 270, 315] }) // your own
.pegs(false) // none: a flapping pointer stays at rest
- Leave
anglesout and the pegs follow the dividers, so dynamic sections carry their pegs with them as the boundaries move. - Give
anglesand they are wheel-local degrees, fixed. Twice as many pegs as sections is a common trick: twice the ratchet, same landings. insetdefaults totipInset + size - 2: the peg ring lands just inside the tongue’s tip, so a peg’s shoulder bites 2 px into the blade. Set it smaller and the pegs sit out past the tip, where the blade has to lift over them entirely - a much bigger flap, andmaxAnglehas to allow it.sizeis the rest of the feel: a bigger peg starts pushing earlier and holds on longer.GraphicsRingSkindraws them withpegs: true; a skin of your own readsctx.pegs. The debug overlay’spegslayer always draws them, plus the contact zone and the peg being ridden.
One peg, start to finish#
The blade and the peg are two solids that may not overlap, and that is the whole model. The blade is a triangle hinged at the pin, tipWidth across at its base, tapering to a point at the tip; the peg is a circle. Every frame the engine asks one question - what is the smallest swing that keeps the circle outside the triangle - and gives the tongue exactly that.
Three things follow, and they are the three things a flapper does:
- It yields as far as the peg needs and no further. Not a curve fitted to look about right: the angle is the geometry.
- It never cuts through a peg. There is no setting, no speed and no frame rate at which it can.
- It does not fall until the peg has gone. On the way down the spring does the work, but it may only descend into clear air, so a blade resting on a peg comes down as the peg leaves and not one degree before.
- It rides, it is not thrown. While a peg carries it the blade has no velocity of its own, so when the peg finally lets go the spring starts from rest: one clean swing home instead of a flung overshoot and a chatter against the peg it just cleared.
Both knobs that soften the contact are padding on the peg rather than fudge on the angle. elasticity makes the blade treat every peg as fatter, so it swings wider; friction adds padding that has to clear as well, so the peg drags the blade further past itself before letting go. Either way the blade is measured against something at least as big as the peg, which is why it can never touch one.
A wheel standing still with a peg under the tongue leans the tongue off it, to whichever side it was already on. A blade resting inside a peg is the one arrangement that never happens.
How far it swings comes from how deep the tip dips. A peg whose shoulder bites 2 px into the blade - what .pegs() gives you by default - asks about 13 degrees of a 60 px tongue. Push the pegs out past the tip and the blade has to lift clear over them entirely, which is a much bigger flap; maxAngle caps it, and the engine says so once when the cap makes clearing impossible.
| Knob | Feel | Try |
|---|---|---|
elasticity | how far it yields: padding on the peg | 1 exactly enough, 1.4 swings wide and floppy. Under 1 is clamped |
friction | how long the peg drags it on: more padding | 0 lets go the moment the peg is through, 1 hangs on a long way |
stiffness | how hard it snaps back | 140 rubber, 420 default, 700 steel |
damping | how quickly the ringing dies | 5 rings audibly, 14 default, 18 dead |
maxAngle | the hard limit | 45; set below what the geometry needs and the blade rides through the pegs |
tipWidth | how wide the blade is | 14; a wider blade needs a bigger swing to get out of the way |
drag | how hard it holds the ring back | 0 weightless, 1 the wheel fights every peg |
The lever the peg turns is pinRadius - pegs.radius, so a long tongue swings through a smaller angle for the same shove. flap: false keeps a pointer rigid: no contact, no spring, just a tick.
Pegs on the rim#
The other arrangement: studs right at the edge of the wheel and a short flapper flicking over them, the way a carnival wheel is built. The contact does not change - the blade still may not touch a peg - but the way out does. A peg under the tip cannot slip past it sideways, so the blade rides up until its tip lifts clear over the peg.
Keep the flapper small. The shorter the arm, the further it has to swing to lift the same few pixels, so a rim wheel wants a stubby flapper that flies rather than a long one that heaves: about a quarter of the radius reads right, and it is the arrangement’s whole character.
That is a big swing, and it is the right one: watch any real wheel and the flapper lifts a long way over every peg. The arrangement below asks for about 48 degrees, past the default cap of 45.
.radius(215)
.pegs({ size: 9, inset: 5 }) // centres 5 px inside the rim: on the edge
.pointer({
tipInset: 17, // the tip dips a few px past the studs
skin: { type: 'graphics', shape: 'tongue', length: 50, width: 22 },
flap: { maxAngle: 62, stiffness: 240, damping: 9, friction: 0.25 },
})
Loading recipe…
maxAngle is the thing to get right. This geometry needs 48 degrees; leave the cap at the default 45 and the blade spends part of every peg pinned against its limit, riding through the last few pixels of it. The engine says so once and names the angle it needs. The readout above shows the largest lift of the last spin, so set the cap a little above that.
Between the two arrangements:
| Pegs on the rim | Pegs inside the tip | |
|---|---|---|
pegs.inset | small: 0 to a peg’s width | the default, tipInset + size - 2 |
| Escape | the tip lifts clear over the peg | the peg’s shoulder slips past a shallow bite |
| Arm | short, about a quarter of the radius | whatever suits the art |
| Swing | 40 to 60 degrees | 10 to 20 |
maxAngle | set it deliberately, well above the lift | the default has room |
| Reads as | a carnival wheel, a big slow clack | a tight ratchet |
The release is a spring#
At speed a peg crosses the whole contact inside a single frame. Nothing would ever be seen pushing, so the tongue is flicked straight to the angle that peg would have held it at - which is why a fast wheel keeps it pinned and jittering while a crawling one bends slowly over every peg and falls all the way back.
Three tongues against one set of pegs: a stiff short one at the top, a floppy one with heavy drag at four o’clock, a rigid triangle at eight.
Loading recipe…
Drag: the tongue slowing the ring#
A real flapper is in the way. The peg has to shove it aside, and while it is shoving, the wheel is not turning as freely as it wants to.
.pointer({ flap: { drag: 1, dragRelease: 18 } })
drag is 0 by default. At 1 the tongue can hold the ring back by a whole contact width of arc - a couple of degrees on a normal wheel - and releases it when the peg wins. The two wheels below run the same spin to the same section at the same time; only the right one drags.
Loading recipe…
Worth being exact about what this is, because it would be easy to oversell: the hold is drawn, not planned. rotationDeg never sees it, the ticks never see it, and the landing never sees it. It always relaxes to zero, so the wheel comes to rest drawn exactly on its result - the one thing a bonus wheel may never get wrong. Read ring.visualRotationDeg for where the disc actually is and ring.dragDeg for how much is being held; the debug HUD prints the second one while it is non-zero.
If you want the wheel to genuinely take longer to stop, that is a spin profile and a stop plan, not the tongue.
The art#

Four tongues, one set of rules - all 160 x 260, tip up, pin at 0.5 / 0.85
download all (1277 KB zip)Because all four share the pin and the tip edge, swapping one for another is a one-line change and nothing else moves. MIT, no attribution; the zip has the disc art too.
new TexturePointerSkin({
texture,
artDirection: 'up', // where the tip is in the source image
pin: { x: 0.5, y: 0.85 }, // fractions of the image: the pivot
scale: 0.42,
length: 96, // pin to tip after scaling; defaults to the edge distance
})
| Requirement | Why |
|---|---|
Tip on one edge, stated as artDirection | The engine rotates the art so the tip faces the hub |
| Pin as fractions, not pixels | The art can be re-exported at any size and still pivot correctly |
| A few transparent px at the tip | A trimming packer will otherwise clip the point off |
| Authored 2x | It is the one sprite the player stares at while the wheel slows down |
| Nothing important near the pin | It is hidden under the bezel on most layouts |
The gem-tipped stopper from Wheel of Happiness is declared exactly the same way:

Three tongues, three pipelines, one set of pegs - drawn with Graphics, a PNG, and the studio’s own art:
Loading recipe…
A Spine tongue#
import { SpinePointerSkin } from 'pixi-wheels/spine';
.pointer({
angle: 90,
skin: new SpinePointerSkin({
skeleton: 'stopperData', atlas: 'wheelAtlas',
length: 80, // required: a skeleton has no obvious tip
artDirection: 'up',
idleAnimation: 'idle',
tickAnimation: 'tick', // one shot per divider
flapRotates: true, // false when the tick animation does the swinging
}),
})
length is not optional here: the engine has no way to guess how far the tip is from the pin in a skeleton. flapRotates: false is the setting to reach for when the studio’s tick animation already swings the whole stopper - otherwise the spring and the animation fight and the result reads as a stutter. The Super Wheel recipe runs one against an authored skeleton.
Events#
wheel.events.on('pointer:tick', ({ ring, pointer, from, to, speed, direction }) => {
// one event per divider, in order, however fast the ring turns
});
| Field | What it carries |
|---|---|
ring | Ring id, so a two-ring wheel can tell its tongues apart |
pointer | Pointer id: every pointer ticks on its own |
from, to | The resolved sections either side of the divider that just passed |
speed | Ring speed at the crossing, deg/s, unsigned - the input to pitch and volume |
direction | Which way it was actually turning at that moment |
A single frame that sweeps four dividers reports four crossings, in order, so a counter is always right even at 900 deg/s. Between events, pointer.deflection and pointer.engagedPeg are live.
The sound#
The ratchet is the whole soundtrack of a wheel slowing down, and it is the easiest thing to get wrong: a peg event per divider at speed is sixty voices a second.
Loading recipe…
- Rate-gate. One click per 45 ms at most. Past that the ear hears a buzz, and every extra voice is a real cost.
- Ride the speed. Pitch and volume from
speed, plus a few percent of jitter, so no two clicks are the same sample twice. - Swap low. Under about 120 deg/s, a heavier, lower sample. That single swap is most of what makes a wheel sound like it is running out of energy.
- Let the wheel emit. The audio layer listens; nothing in a sound hook should call a wheel method. A skip and a slam both arrive at
spin:landingon their own.
The events and audio guide has the full cue sheet, and the sound hooks recipe wires a whole game’s set through @schmooky/zvuk.
Debugging it#
Press Debug on any demo here. The pegs layer draws every peg, the contact zone either side of the tip, and fills the peg currently being ridden; the HUD prints flap <deg> on peg <n> and, when drag is on, how much arc is being held. In the console:
wheel.main.pointers[0].deflection // degrees, live
wheel.main.pointers[0].engagedPeg // index into ring.pegs.angles, or null
wheel.main.pegs // { size, radius, angles }
wheel.main.dragDeg // arc held back right now
debugSnapshot(wheel) // all of it as plain JSON
The checklist#
| Decision | Default | Reach for |
|---|---|---|
| Where it reads | angle: -90 | 90 for a slot that reads from six o’clock |
| How deep | tipInset: 18 | Deeper on a wheel with a thick bezel |
| Pegs | one per divider | Custom angles for a ratchet finer than the sections |
| Feel | elasticity 1, friction 0.35 | Softer and floppier for a big cartoon flapper |
| Spring | stiffness 420, damping 14 | 140 / 5 for rubber, 700 / 18 for steel |
| Weight | drag: 0 | 0.6 - 1 when the stop should look like work |
| Art | Graphics | A PNG with pin and artDirection, or a skeleton with tick |
| Sound | - | One gated click per peg, pitched by speed |


