Skip to content
pixi-silk
GitHub repository
Interface

Layout and smart containers

Place blocks on portrait and landscape screens with SmartContainer records and breakpoints, then lay out what is inside them with flexbox. pixi-silk/ui/layout.

Updated pixi-silk 1.0.1View as Markdown

SmartContainer places a whole block on the screen, such as a corner readout, the main view or a bottom bar, on portrait and landscape screens. FlexContainer arranges children inside a block with flexbox. Both come from pixi-silk/ui/layout.

Place blocks on portrait and landscape screens#

Pick a device. Tick debug, or press L over the demo, for the layout overlay.

The screen above is five SmartContainers. One readout pins to the top left and the menu to the top right. The main view stays centred, the bar runs along the bottom and the background covers the whole window.

import { SmartContainer } from 'pixi-silk/ui/layout';

const main = new SmartContainer({
    viewport: app,
    portraitData: { width: 1040, height: 640, correctionOffsetY: -200 },
    landscapeData: { width: 1040, height: 640, correctionOffsetY: -50 },
});

app.stage.addChild(main);

The container follows app and lays out again on every resize. It uses portraitData when the window is taller than wide and landscapeData otherwise. Without a viewport a SmartContainer does nothing: it has no window to place itself in.

The scale is the window size over the reference resolution, the smaller ratio, or the larger one with fitCover. Stretch replaces it per axis, and the corrections apply last. placeBox(record, window) gives the same placement without a container.

Layout records#

FieldDefaultWhat it does
width, height0The design box, in design units
viewportWidth, viewportHeight1080x1920 portrait, 1920x1080 landscapeThe reference resolution
align, valigncenterAnchor of the scaled box: left, center, right and top, center, bottom
fitCoverfalseCover the window instead of fitting inside it
stretchWidth, stretchHeightfalseScale one axis to span the window. Distorts the content, which suits backgrounds
extendWidth, extendHeightfalseKeep the uniform scale and grow the box to the window. A flex layout spreads out
correctionScaleX, correctionScaleY1Multiply the scale
correctionOffsetX, correctionOffsetY0Move the box, in design units
ignoreSafeAreafalsePlace the box in the whole window, under notches
layoutthe container’sFlex style for the children while this record is active

Container options#

OptionDefaultWhat it does
viewportnoneAn Application or renderer (follows resize), a signal of a rectangle, or a plain rectangle (call updateContainer() after a change)
portraitData, landscapeDatanoneThe two orientation records
breakpointsnoneNamed records with a when(width, height) test, tried in order before the orientations
safeAreanoneInsets in screen pixels that shrink the window boxes are placed in
switchTransitionfalseGlide on a spring to the new place and scale when the breakpoint changes. Resizes inside one breakpoint snap
layoutnoneTurns the design box into a flex container
import { SmartContainer } from 'pixi-silk/ui/layout';

const background = new SmartContainer({
    viewport: app.renderer,
    safeArea: { top: 44, bottom: 34 },
    breakpoints: [
        { name: 'ultrawide', when: (w, h) => w / h > 2.1, data: { width: 2560, height: 1080, viewportWidth: 2560, fitCover: true } },
    ],
    portraitData: { width: 1080, height: 1920, fitCover: true, ignoreSafeArea: true },
    landscapeData: { width: 1920, height: 1080, fitCover: true, ignoreSafeArea: true },
});

On a window wider than 2.1:1 the ultrawide record wins. Otherwise the orientation decides.

Lay out the children of a box#

Without a layout, children stay where you put them, in design units. With one, the design box is a flex container, and media rules can switch its style per breakpoint:

import { SmartContainer } from 'pixi-silk/ui/layout';
import { Container } from 'pixi.js';

const bar = new SmartContainer({
    viewport: app,
    layout: {
        padding: [16, 32, 28],
        gap: 24,
        alignItems: 'center',
        justifyContent: 'space-between',
        media: [{ breakpoint: 'portrait', style: { wrap: 'wrap', justifyContent: 'center' } }],
    },
    portraitData: { valign: 'bottom', width: 1080, height: 420, extendWidth: true },
    landscapeData: { valign: 'bottom', width: 1920, height: 200, extendWidth: true },
});
const play = new Container();

bar.add(play, { media: [{ breakpoint: 'portrait', style: { basis: '100%' } }] });

Arrange children with flexbox#

Change the item count, direction, gap and alignment. Hover a card to read its box.
import { FlexContainer } from 'pixi-silk/ui/layout';
import { Sprite, Text } from 'pixi.js';

const row = new FlexContainer({ layout: { gap: 12, padding: 16, alignItems: 'center' } });

row.add(Sprite.from('coin.png'), { width: 48, height: 48, fit: 'contain' });
row.add(new Text({ text: 'Balance' }), { grow: 1 });
row.resize(480, 80);

The coin takes a 48 by 48 box and the label takes the rest of the row. Call resize(width, height) on a root FlexContainer, or nest it in another FlexContainer or a SmartContainer. A SmartContainer inside a FlexContainer places itself and takes no space.

Give a child a style with add(child, style) or setLayout(child, style). A child is measured by its local bounds, or by the style’s measure (for wrapped text, use the wrapText helper). Pixi Text and SilkText both report real text metrics. The style fields follow CSS flexbox and are listed in Layout engine.

The container lays out once per frame, and only after a change: a child added, removed or restyled, a new size, or invalidate(child) after new text or a new texture. An idle container is off the ticker.

OptionDefaultWhat it does
layoutnoneThe container’s own flex style
transitionfalseChildren glide to their new places on springs, scale included. true is stiffness 170, damping 26
watchfalseCheck the children’s bounds every frame and lay out again when they change
onLayoutnoneCalled after every layout pass
tickerTicker.sharednull to call tick() yourself

A child with animate: false never glides. A child with onSize is called with its box size whenever that changes, which suits a panel background. Such a child measures as zero, so size it with inset, grow or a size:

import { SilkGraphics } from 'pixi-silk';
import { FlexContainer } from 'pixi-silk/ui/layout';

const panel = new FlexContainer({ layout: { padding: 16 } });
const bg = new SilkGraphics();

panel.add(bg, {
    position: 'absolute',
    inset: 0,
    onSize: (w, h) => bg.clear().roundRect(0, 0, w, h, 18, 0.6).fill(0x1c1c1e),
});

Fit art and shrink text into a box#

Change the box size and move fitAlign. The last box holds a long text.

fit scales a child into its box, like CSS object-fit.

ValueWhat it does
noneKeeps the child’s size. The default
containFits inside the box
coverFills the box and overflows. Mask the box, not the child: a masked child measures only what its mask shows
scale-downcontain that never enlarges
fillStretches both axes

fitAlign places the child in its box, from [0, 0] (top left) to [1, 1] (bottom right). The default is the centre. minScale and maxScale clamp the fit scale.

Text shrink-to-fit is fit: 'scale-down' with a floor. The text keeps its size while it fits and scales down when its box gets narrow, never below minScale:

import { FlexContainer } from 'pixi-silk/ui/layout';
import { SilkText } from 'pixi-silk/text';

const pill = new FlexContainer({ layout: { direction: 'column', padding: [10, 18] } });

pill.add(new SilkText({ text: '$1,250,000.00', fontSize: 30 }), { fit: 'scale-down', minScale: 0.55 });

Responsive styles#

Any style can carry portrait and landscape values, and media rules. A rule matches an orientation, a breakpoint name, or a minWidth and maxWidth in design units. Rules apply in order over the base values. Width and orientation rules test the root container’s layout area.

import { FlexContainer } from 'pixi-silk/ui/layout';

const main = new FlexContainer({
    layout: {
        gap: 20,
        landscape: { direction: 'row' },
        portrait: { direction: 'column' },
        media: [{ maxWidth: 600, style: { gap: 10 } }],
    },
});
const banner = new FlexContainer();

main.add(banner, { landscape: { display: 'none' } });

The banner shows in portrait only. A child with display: 'none' is hidden as well as taken out of the flow.

Debug and preview devices#

import { layoutDebug, SmartContainer, simulateViewport } from 'pixi-silk/ui/layout';

const debug = layoutDebug(app, { key: 'KeyL' });
const sim = simulateViewport({ area: app.screen, device: 'phone portrait' });
const hud = new SmartContainer({ viewport: sim, portraitData: { width: 1080, height: 300, valign: 'bottom' } });

sim.content.addChild(hud);
app.stage.addChild(sim);
sim.setDevice('phone landscape');

layoutDebug draws over the stage and is off until you toggle it with the key. For a SmartContainer it shows the design box with its breakpoint and scale, the reference resolution in purple, the safe area in orange and stretched axes in yellow. For a FlexContainer it shows boxes in blue, padding in green, gaps in pink and real bounds in white, or red when a child overflows.

simulateViewport shows a device window inside a desktop page, scaled to fit and letterboxed. Pass it as the viewport of your SmartContainers and add them to its content. DEVICES lists the preset sizes, and a [width, height] pair works too.

Where it stops#

The layout runs on the CPU in your frame, once per change. Rotated children keep their box but are measured unrotated. The engine has no order, overflow or borders, and an item’s default minimum size is 0, not its content. Layout engine lists every difference from CSS.

API reference: SmartContainer, FlexContainer, layoutDebug, simulateViewport, placeBox