Pointers
.pointer({
id: 'top', // default 'pointer' for the first
angle: -90, // screen degrees: -90 top, 0 right, 90 bottom
facing: 'inward', // sits on the rim, tip toward the hub; 'outward' sits at the hub
tipInset: 18, // how far the tip reaches into the sections
flap: { elasticity: 1, friction: 0.35, stiffness: 420, damping: 14, maxAngle: 45, tipWidth: 14, drag: 0 },
skin: new GraphicsPointerSkin({ shape: 'tongue' | 'triangle' | 'needle', length: 72, width: 36, pinRadius: 8, color: 0xffffff }),
})
Results are read against the first pointer. Add more for decoration or a second read position; each one ticks.
Pegs and the tongue#
Every ring has pegs: small circles on the disc, one per divider, that follow dynamic sections. .pegs({ size, inset, angles }) changes them, .pegs(false) removes them (a flapping pointer then stays at rest). inset defaults to tipInset + size - 2, so a peg’s shoulder bites 2 px into the first tongue’s blade. The debug overlay draws them (pegs layer); GraphicsRingSkin does too with pegs: true.
The contact is two solids that may not overlap: the blade is a triangle hinged at the pin, tipWidth across at its base and a point at the tip; the peg is a circle. Each frame the tongue takes the smallest swing, from where it already stands, that keeps the circle outside the triangle. It yields as far as the peg needs, never cuts through one, and falls back under stiffness and damping only into clear air. At speed a peg crosses the whole contact inside one frame and the tongue is flicked to the angle that peg would have held it at.
| Knob | Feel |
|---|---|
elasticity | how far it yields: padding on the peg. 1 exactly enough, 1.4 wide and floppy. Under 1 is clamped |
friction | more padding: the peg drags the blade further past itself before letting go |
stiffness | how hard it snaps back |
damping | how quickly the ringing dies |
maxAngle | the hard limit. Below what the geometry needs, the blade rides through the pegs and warns once |
tipWidth | how wide the blade is |
drag | how hard the tongue holds the ring back, 0 off, 1 the wheel fights every peg |
pegs size / inset | a bigger peg pushes earlier; pegs out past the tip need a much bigger flap |
flap: false keeps a pointer rigid. pointer.deflection and pointer.engagedPeg are live; the HUD prints both.
drag is the one knob that touches the ring: while a peg climbs the tip, the disc is drawn up to a contact width of arc behind itself and springs forward at dragRelease when the peg wins. Drawn only: rotationDeg, the ticks and the landing never see it, and it relaxes to zero at rest. ring.visualRotationDeg and ring.dragDeg report it.
Ticks#
wheel.events.on('pointer:tick', ({ pointer, from, to, speed, direction }) => {
audio.play('tick', { volume: Math.min(1, speed / 700) });
});
One event per divider, in order, however fast the ring turns; a frame that sweeps several dividers reports each of them.
Texture and Spine pointers#
new TexturePointerSkin({ texture, artDirection: 'up', pin: { x: 0.5, y: 0.85 }, scale: 1 })
The pin is the point the art pivots around; artDirection says which way the tip points in the source image. SpinePointerSkin (from pixi-wheels/spine) plays a tick animation on every crossing and an idle loop otherwise.
Art requirements, contact diagrams, events and sound are in the tongue recipe.