Layer groups
$.group(properties, settings, fn) creates a container layer. The children authored inside fn are composited onto a private project-sized surface, then the group's own transform / opacity / transitions / effects pipeline runs on that surface — exactly as if the group were a single visual layer.
On this page
const $ = new VideoFlow({ width: 512, height: 512, fps: 30, backgroundColor: '#0a0d18' });
// One transition + one rotation animation applies to the whole sub-tree.
const card = $.group(
{ position: [0.5, 0.5] },
{
transitionIn: { transition: 'overshootPop', duration: '600ms', params: { from: 0.6 } },
transitionOut: { transition: 'blurResolve', duration: '450ms' },
},
() => {
$.addShape(
{
width: 50, height: 32,
fill: '#0e1524',
strokeColor: '#4ecdc4', strokeWidth: 0.25,
cornerRadius: 2.5,
},
{ shapeType: 'rectangle' },
);
$.addText({
text: 'CARD', fontSize: 2, fontWeight: 700, color: '#4ecdc4',
position: [0.5, 0.40], letterSpacing: 0.2,
});
$.addText({
text: '128', fontSize: 11, fontWeight: 900, color: '#f1f5f9',
position: [0.5, 0.58],
});
$.wait('2.6s'); // group's duration auto-derives from this
},
);
card.animate(
{ rotation: -3 },
{ rotation: 3 },
{ duration: '2.6s', easing: 'easeInOut', wait: false },
);
return $;Why groups?
- Apply one transition to a whole card scene instead of per-layer.
- Animate
scale/rotation/opacityon the composite. - Run a GLSL effect over the entire sub-tree (one bloom pass over a card, not three).
Auto timing
A group's startTime is the current flow time when $.group(...) is called. Its sourceDuration auto-derives from the latest child's end. Children authored inside fn see the group-relative timeline starting at 0.
$.group(
{ position: [0.5, 0.5], scale: 1 },
{
transitionIn: { transition: 'slideLeft', duration: '600ms' },
transitionOut: { transition: 'zoom', duration: '600ms' },
},
(card) => {
// Children's timing is relative to the group — `0` here is the
// group's start, not the project start.
$.addImage({ fit: 'cover', opacity: 0.55 }, { source: 'https://videoflow.dev/samples/sample.jpg' });
$.addText({ text: 'Card', fontSize: 4, color: '#fff' });
$.wait('400ms');
$.addText({ text: 'Composite as one', fontSize: 1.4, color: '#cdd5e1', position: [0.5, 0.6] });
},
);Animating the group
The callback receives the group layer itself, so you can animate it from inside the block. Use wait: false so the animation doesn't advance the group's local flow pointer.
$.group(
{ position: [0.5, 0.5] },
{},
(card) => {
card.animate({ scale: 1, rotation: -3 }, { scale: 1.04, rotation: 3 }, { duration: '3s', wait: false });
$.addShape({ width: 40, height: 24, fill: '#1c2233', cornerRadius: 1.5 }, { shapeType: 'rectangle' });
$.addText({ text: '128k', fontSize: 5, fontWeight: 800, color: '#fff', position: [0.5, 0.5] });
$.wait('3s'); // group's sourceDuration auto-derives from this
},
);Nested groups
Groups compose. A nested badge group has its own transition and its own timeline; it composites along with its parent and inherits the parent's transform.
$.group({}, {}, () => {
$.addShape({ width: 40, height: 24, fill: '#1c2233' }, { shapeType: 'rectangle' });
$.group(
{ position: [0.7, 0.32], scale: 0.9 },
{ transitionIn: { transition: 'zoom', duration: '350ms' } },
() => {
$.addShape({ width: 8, height: 3, fill: '#4ecdc4', cornerRadius: 1.5 }, { shapeType: 'rectangle' });
$.addText({ text: 'NEW', fontSize: 0.9, fontWeight: 800, color: '#0a0d18' });
},
);
});