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#
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#
| Field | Default | What it does |
|---|---|---|
width, height | 0 | The design box, in design units |
viewportWidth, viewportHeight | 1080x1920 portrait, 1920x1080 landscape | The reference resolution |
align, valign | center | Anchor of the scaled box: left, center, right and top, center, bottom |
fitCover | false | Cover the window instead of fitting inside it |
stretchWidth, stretchHeight | false | Scale one axis to span the window. Distorts the content, which suits backgrounds |
extendWidth, extendHeight | false | Keep the uniform scale and grow the box to the window. A flex layout spreads out |
correctionScaleX, correctionScaleY | 1 | Multiply the scale |
correctionOffsetX, correctionOffsetY | 0 | Move the box, in design units |
ignoreSafeArea | false | Place the box in the whole window, under notches |
layout | the container’s | Flex style for the children while this record is active |
Container options#
| Option | Default | What it does |
|---|---|---|
viewport | none | An Application or renderer (follows resize), a signal of a rectangle, or a plain rectangle (call updateContainer() after a change) |
portraitData, landscapeData | none | The two orientation records |
breakpoints | none | Named records with a when(width, height) test, tried in order before the orientations |
safeArea | none | Insets in screen pixels that shrink the window boxes are placed in |
switchTransition | false | Glide on a spring to the new place and scale when the breakpoint changes. Resizes inside one breakpoint snap |
layout | none | Turns 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#
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.
| Option | Default | What it does |
|---|---|---|
layout | none | The container’s own flex style |
transition | false | Children glide to their new places on springs, scale included. true is stiffness 170, damping 26 |
watch | false | Check the children’s bounds every frame and lay out again when they change |
onLayout | none | Called after every layout pass |
ticker | Ticker.shared | null 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#
fit scales a child into its box, like CSS object-fit.
| Value | What it does |
|---|---|
none | Keeps the child’s size. The default |
contain | Fits inside the box |
cover | Fills the box and overflows. Mask the box, not the child: a masked child measures only what its mask shows |
scale-down | contain that never enlarges |
fill | Stretches 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