Layer groups
Notification and metric cards built from nested layer groups.
// Three card groups stagger in (zoom + 500ms delay), drift on a subtle 3D
// tilt, count their headline number up from 0, then exit together.
// Demonstrates `$.group()`, nested layers, `numberCountUp`, and `$.parallel`.
const $ = new VideoFlow({
name: 'Groups',
width: 1920,
height: 1080,
fps: 30,
backgroundColor: '#0a0d18',
});
// Per-card config. rotFrom/rotTo are [rx, ry, rz] in degrees — small values
// keep the tilt subtle. Each card uses a different axis combo for variety.
const CARDS = [
{ x: 0.20, label: 'BOOKS READ', value: '24', color: '#ff5a1f', rotFrom: [5, -12, 0], rotTo: [-5, 12, 0] },
{ x: 0.50, label: 'MOVIES SEEN', value: '87', color: '#4ecdc4', rotFrom: [-10, 5, 0], rotTo: [10, 10, 0] },
{ x: 0.80, label: 'SONGS LIKED', value: '342', color: '#a78bfa', rotFrom: [-8, 10, 0], rotTo: [8, -10, 0] },
];
const groups = [];
for (const card of CARDS) {
// $.group(props, settings, fn, options) — `fn` builds the children inside
// the group's local time origin (their startTime is relative to the group).
const g = $.group(
// perspective enables 3D depth for child rotation.
{ position: [card.x, 0.5], perspective: 20 },
{
transitionIn: { transition: 'zoom', duration: '600ms', params: { from: 0.6 } },
transitionOut: { transition: 'zoom', duration: '500ms', params: { from: 0.6 } },
},
() => {
// Rounded square frame.
$.addShape(
{
width: 30, height: 30,
fill: '#0e1524',
strokeColor: card.color, strokeWidth: 0.2,
cornerRadius: 3,
position: [0.5, 0.5],
},
{ shapeType: 'rectangle' },
);
// Eyebrow label.
$.addText({
text: card.label,
fontSize: 1.8, fontWeight: 700,
color: card.color, position: [0.5, 0.42],
letterSpacing: 0.2,
});
// Big number — `numberCountUp` ramps the digits from 0 → final value
// over the transitionIn duration.
$.addText(
{
text: card.value,
fontSize: 8, fontWeight: 900,
color: '#f1f5f9', position: [0.5, 0.55],
},
{ transitionIn: { transition: 'numberCountUp', duration: '1s' } },
);
},
// `waitFor: 0` so the loop's $.wait('500ms') alone controls the stagger.
{ waitFor: 0 },
);
// Slow 3D drift on the *group* — applies to every child layer at once.
g.animate(
{ rotation: card.rotFrom },
{ rotation: card.rotTo },
{ duration: '5s', easing: 'easeInOut', wait: false },
);
groups.push(g);
$.wait('500ms');
}
$.wait('4s');
// $.parallel runs each branch from the current flow time — all three exits
// fire on the same frame, then each plays its own declared transitionOut.
$.parallel(groups.map((g) => () => g.remove()));
$.wait('500ms');
return $;