playTimeline function
An arrangement as something that plays.
Nothing is resolved here: a placed group IS the group, and a group holds
the node it animates. Binding is building the tree, which is why a
compiled motion needs no document and no scene to play — it hands its
own timeline over.
into caches one BoundGroup per group, so a group placed twice writes
through one writer rather than two that fight.
Implementation
Playable playTimeline(
TimelineExpr expr, {
Map<AnimateGroup, BoundGroup>? into,
}) {
var bound = into ?? <AnimateGroup, BoundGroup>{};
Playable build(TimelineExpr e) => switch (e) {
AnimateGroup g => bound.putIfAbsent(g, () => BoundGroup._(g)),
ParExpr p => _Par([for (var c in p.children) build(c)]),
SeqExpr s => _Seq([for (var c in s.children) build(c)]),
AtExpr a => _At(a.offset, build(a.child)),
SpeedExpr s => _Speed(s.factor, build(s.child)),
RepeatExpr r => _Repeat(r.times, build(r.child)),
};
return build(expr);
}