edit method
Implementation
@override
Reel edit(Take take) {
var screen = take.screen;
var camera = FrameNode(name: 'camera')
..x = 0
..y = 0
..width = screen.width
..height = screen.height
..clip = true
..children.addAll([
ExternalNode(const ScreenArgs(), name: 'screen')
..width = screen.width
..height = screen.height,
ExternalNode(const PointerArgs(), name: 'pointer')
..width = screen.width
..height = screen.height,
]);
var root = FrameNode(name: 'root')
..width = screen.width
..height = screen.height
..fill = ground
..children.add(camera);
// The document is made *after* the walk below has added its captions:
// a document adopts the nodes it is given at construction, and a node
// added later has no document to tell when its fx move.
var motion = MotionDocument(sceneClassName: 'StockReel');
var placed = <TimelineExpr>[];
var b = ReelBuilder(take);
var captions = 0;
var held = false;
for (var beat in take.beats) {
switch (beat) {
case Said(cue: ScenarioTitle(:var text)):
// A lower third: a dark band and the words on it, because a stage
// that knows nothing about the app cannot know what colour the app
// is under the caption, and white on a white screen is no caption.
// Both nodes are authored at full opacity and hidden by the track:
// opacity fx *multiplies*, so a node authored at 0 stays at 0
// whatever a track says; the track's first key is 0 and `At` clamps
// to it before the window opens, which keeps the caption unseen
// until its cue.
var n = captions++;
var band = ShapeNode(name: 'captionBand$n', corner: 10)
..x = 16
..y = screen.height - 108
..width = screen.width - 32
..height = 64
..fill = const SceneColor(0xCC15161A);
var caption = TextNode(text, name: 'caption$n')
..x = 32
..y = screen.height - 94
..width = screen.width - 64
..fontSize = 28
..weight = SceneFontWeight.w700
..color = const SceneColor(0xFFFFFFFF);
root.children.addAll([band, caption]);
List<MotionKey> fadeKeys() => [
MotionKey(at: Duration.zero, value: 0.0),
MotionKey(at: const Duration(milliseconds: 350), value: 1.0),
MotionKey(
at: titleFor - const Duration(milliseconds: 350),
value: 1.0,
),
MotionKey(at: titleFor, value: 0.0),
];
var fadeBand = AnimateGroup(band, name: band.name)
..tracks['opacity'] = MotionTrack(fadeKeys());
var fade = AnimateGroup(caption, name: caption.name)
..tracks['opacity'] = MotionTrack(fadeKeys());
// Said partway through a beat that has already been played — the
// title over the opening hold — so it lands where it was said, not
// where the walk has got to.
var at = b.reelTimeOf(beat.at) ?? b.reel;
motion.groups.addAll([fadeBand, fade]);
placed.addAll([AtExpr(at, fadeBand), AtExpr(at, fade)]);
b.say(ScenarioTitle(text), over: titleFor, at: at);
case Tapped(:var target?):
// Start moving before the finger presses: the camera knows what is
// about to happen, which is the whole reason the edit reads a take
// rather than watching a run.
var press = beat.phase(PhaseKind.press) ?? beat.phases.first;
var start = b.reel + (press.at - beat.at) - pushIn;
if (start < Duration.zero) start = Duration.zero;
var off = cameraOffset(
screen: screen,
toward: target.center,
zoom: zoom,
);
var end = beat.duration;
var back = end - pushIn;
if (back < pushIn) back = pushIn;
var push = AnimateGroup(camera, name: 'push${beat.at.inMilliseconds}')
..tracks['scale'] = MotionTrack([
MotionKey(
at: Duration.zero,
value: 1.0,
curve: SceneCurves.easeOut,
),
MotionKey(at: pushIn, value: zoom),
MotionKey(at: back, value: zoom, curve: SceneCurves.easeInOut),
MotionKey(at: end, value: 1.0),
])
..tracks['translateX'] = MotionTrack([
MotionKey(
at: Duration.zero,
value: 0.0,
curve: SceneCurves.easeOut,
),
MotionKey(at: pushIn, value: off.dx),
MotionKey(at: back, value: off.dx, curve: SceneCurves.easeInOut),
MotionKey(at: end, value: 0.0),
])
..tracks['translateY'] = MotionTrack([
MotionKey(
at: Duration.zero,
value: 0.0,
curve: SceneCurves.easeOut,
),
MotionKey(at: pushIn, value: off.dy),
MotionKey(at: back, value: off.dy, curve: SceneCurves.easeInOut),
MotionKey(at: end, value: 0.0),
]);
motion.groups.add(push);
placed.add(AtExpr(start, push));
b.playBeat(beat);
if (!held) {
held = true;
b.hold(holdAfterFirstTap, after: beat);
}
case _:
b.playBeat(beat);
}
}
// The end: the last frame held, under the scenario's name.
var closing = TextNode(take.scenario, name: 'closing')
..x = 24
..y = screen.height / 2 - 20
..width = screen.width - 48
..fontSize = 32
..weight = SceneFontWeight.w700
..color = const SceneColor(0xFFFFFFFF);
root.children.add(closing);
var dim = AnimateGroup(camera, name: 'dim')
..tracks['opacity'] = MotionTrack([
MotionKey(at: Duration.zero, value: 1.0),
MotionKey(at: const Duration(milliseconds: 500), value: 0.35),
]);
var reveal = AnimateGroup(closing, name: 'reveal')
..tracks['opacity'] = MotionTrack([
MotionKey(at: Duration.zero, value: 0.0),
MotionKey(at: const Duration(milliseconds: 500), value: 1.0),
]);
motion.groups.addAll([dim, reveal]);
placed.add(AtExpr(b.reel, dim));
placed.add(AtExpr(b.reel, reveal));
b.freeze(tail);
motion.timeline = ParExpr(placed);
var scene = SceneDocument(root);
var bound = BoundMotion.bind(motion, scene);
return b.build(stage: SceneStage(scene, motion: bound));
}