Skip to content
pixi-silk
GitHub repository
Interface

Radial menus and buttons

RadialMenu is a marking menu with nested rings and per-device tuning. SilkButton animates in and out and ignores input while it moves or is busy.

Updated pixi-silk 1.0.1View as Markdown

A radial menu puts its items in a ring around the pointer, so a press, a drag towards an item and a release pick it in one gesture. A button here is a widget that knows when it should not take a press.

Open a radial menu#

Press anywhere and drag to an item, or tap to keep the menu open. MOVE and ROTATE open a second ring.
import { bindRadialTrigger, RadialMenu } from 'pixi-silk/ui/radial';

const menu = new RadialMenu({
    items: [
        { id: 'select', label: 'SELECT', icon: drawPointer },
        { id: 'move', label: 'MOVE', icon: drawMove, children: [{ id: 'axis', label: 'Axis' }, { id: 'free', label: 'Free' }] },
        { id: 'delete', label: 'DELETE', icon: drawTrash, disabled: true },
    ],
    sweep: Math.PI * 2, // Math.PI for a half ring
    bounds: { x: 0, y: 0, width: app.screen.width, height: app.screen.height },
    onSelect: (item, path) => console.log(path.map((i) => i.id).join(' > ')),
});

app.stage.addChild(menu);
bindRadialTrigger(toolButton, menu); // or menu.openAt(x, y, event)

A press on toolButton opens the menu at the pointer. onSelect gets the item and the path to it through the rings, so a pick in the second ring logs move > axis.

The menu picks by angle. Past a small dead zone the whole wedge counts, out to the falloff distance, so a fast flick lands on its item even when the pointer overshoots the ring. The current item holds until the pointer is clearly past its edge, so a shaky finger on a boundary does not flicker between two items.

Items with children open the next ring around themselves, to any depth. A full ring slides to stay inside bounds, and a partial ring turns to face the inside instead.

OptionDefaultWhat it does
innerRadius, outerRadius40, 100The first ring
sweepfull turnAngle of the first ring, Math.PI for a half
directionupWhere the ring faces, in radians
closeOnLeavefalseClose once the pointer stays outside the falloff area. By default the menu stays
closeOnSelecttrueClose after a pick
selectParentsfalseItems with children can be picked too
profilesper pointer kindDead zone, falloff, scale, tap time and hysteresis for mouse, pen and touch
hubtrueDraw the centre disc

Touch gets bigger targets and more forgiveness than a mouse. Override any of it with profiles.

Icons are a string, a texture, a container or a function that draws with SilkGraphics in white. The menu tints them, so a hover colour costs no redraw.

Make a button that waits#

import { SilkButton } from 'pixi-silk/ui/button';

const play = new SilkButton({
    shape: 'circle',
    height: 76,
    icon: drawPlay,
    holdMs: 450,
    onPress: () => console.log('play'),
    onHold: () => console.log('play on repeat'),
});
const share = new SilkButton({ label: 'Share' });

await share.hide();        // resolves when the animation ends
play.state.busy.set(true); // spinner, input off
play.state.active.subscribe((on) => console.log('accepts input', on));

A button accepts input only when it is shown, settled, enabled and not busy. That is what keeps a save button from being pressed twice while it saves. show() and hide() return promises that resolve false if the animation was reversed half way. A press that slides a little off the button still counts on release, within hitPadding (6 by default).

ButtonDock from pixi-silk/ui/dock lines buttons up and closes the gap when one hides. Each button’s space follows its animated appearance, so the reflow and the fade move together.

State in signals#

Every widget keeps its state in state: small signals you can read, write and subscribe to. pixi-silk/signal has no Pixi code in it. A signal is a value with subscribe, with no dependency tracking, so it plugs into React, MobX or plain callbacks.

import { batch, computed, signal } from 'pixi-silk/signal';

const volume = signal(1);
const label = computed([volume], () => `volume ${volume.value}`);

volume.subscribe((now, before) => console.log(before, '->', now));
batch(() => {
    volume.set(2);
    volume.set(3);
}); // one notification

computed takes its sources as a list, so it recomputes only when one of them changes. Widgets animate on Ticker.shared and leave the ticker once nothing moves. Pass ticker to use your application’s ticker, or null to step them yourself with tick(dt).

API reference: RadialMenu, bindRadialTrigger, SilkButton, ButtonDock