The tongue (flapper, stopper, clapper) is the fixed thing the wheel is read against, and the one part of a bonus wheel that is allowed to be floppy. This recipe covers all of it; the disc it rides on has its own.
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: 45, 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() // the default: size 6, just inside the first tongue's tip
.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 finds the smallest swing that keeps the circle outside the triangle and gives the tongue exactly that. Four things follow:
- It yields as far as the peg needs and no further. The angle is the geometry, not a curve fitted to look about right.
- It never cuts through a peg, at any setting, speed or frame rate.
- It does not fall until the peg has gone. The spring 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 the release is one clean swing home, not a flung overshoot and a chatter against the peg it just cleared.
The two softening knobs are padding on the peg. 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. A wheel standing still with a peg under the tongue leans the tongue off it, to whichever side it was already on.
How far it swings comes from how deep the tip dips. A peg whose shoulder bites 2 px into the blade, the default, asks about 13 degrees of a 60 px tongue. Pegs out past the tip make the blade lift clear over them entirely, 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, 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. 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; at the default 45 the blade spends part of every peg pinned against its limit, riding through the last few pixels of it, and the engine says so once, naming the angle it needs. The readout above shows the largest lift of the last spin; set the cap a little above it.
| 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, so the tongue is flicked straight to the angle that peg would have held it at: a fast wheel keeps it pinned and jittering, a crawling one bends slowly over every peg and falls all the way back. Three tongues against one set of pegs: a stiff one at the top, a floppy one with heavy friction at four o’clock, a rigid triangle at eight.
Loading recipe…
Drag: the tongue slowing the ring#
A real flapper is in the way: while a peg shoves it aside, 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…
The hold is drawn, not planned. rotationDeg, the ticks and the landing never see it, and it always relaxes to zero, so the wheel comes to rest drawn exactly on its result. ring.visualRotationDeg is where the disc actually is, ring.dragDeg how much is being held; the debug HUD prints the second while it is non-zero. A wheel that should genuinely take longer to stop is a spin profile, 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)All four share the pin and the tip edge, so swapping one for another moves nothing else. 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 required: the engine cannot guess how far the tip is from the pin in a skeleton. Set flapRotates: false when the studio’s tick animation already swings the whole stopper, or the spring and the animation fight. 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 soundtrack of a wheel slowing down, and the easiest thing to get wrong: a click 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 |


