TimeConsumer¶
The fundamental frame-based animation driver
TimeConsumer is the core widget for creating frame-based animations in Fluvie. It rebuilds on every frame, providing the current frame number and progress value to its builder function.
Table of Contents¶
Overview¶
TimeConsumer listens to frame changes and rebuilds its child with updated timing information. This is how you create custom animations that respond to the video timeline.
TimeConsumer(
builder: (context, frame, progress) {
// frame: current frame number (0, 1, 2, ...)
// progress: 0.0 at start, 1.0 at end
return Opacity(
opacity: progress,
child: Text('Frame: $frame'),
);
},
)
When to Use¶
Use TimeConsumer when:
- Creating custom animations not covered by AnimatedProp
- Needing direct access to frame numbers
- Building complex, multi-property animations
- Creating procedural effects
Use AnimatedProp or AnimatedText instead when:
- Using standard animation patterns (fade, slide, scale)
- Simpler code is preferred
Properties¶
| Property | Type | Default | Description |
|---|---|---|---|
builder |
Widget Function(BuildContext, int frame, double progress) |
required | Builder function called every frame |
Builder Parameters¶
| Parameter | Type | Description |
|---|---|---|
context |
BuildContext |
Standard Flutter context |
frame |
int |
Current absolute frame number |
progress |
double |
0.0-1.0 progress through composition |
Examples¶
Basic Fade Animation¶
TimeConsumer(
builder: (context, frame, progress) {
return Opacity(
opacity: progress, // Fades in as video progresses
child: Text('Fading in...'),
);
},
)
Color Animation¶
TimeConsumer(
builder: (context, frame, progress) {
final color = Color.lerp(
Colors.blue,
Colors.red,
progress,
)!;
return Container(
color: color,
child: Center(child: Text('Color shift')),
);
},
)
Position Animation¶
TimeConsumer(
builder: (context, frame, progress) {
// Move from left to right
final x = 100 + (progress * 500);
return Positioned(
left: x,
top: 100,
child: CircleWidget(),
);
},
)
Complex Multi-Property Animation¶
TimeConsumer(
builder: (context, frame, progress) {
// Eased progress
final easedProgress = Curves.easeOutCubic.transform(progress);
// Multiple animated properties
final opacity = easedProgress;
final scale = 0.5 + (0.5 * easedProgress);
final translateY = 50 * (1 - easedProgress);
return Transform.translate(
offset: Offset(0, translateY),
child: Transform.scale(
scale: scale,
child: Opacity(
opacity: opacity,
child: MyWidget(),
),
),
);
},
)
Frame-Based Logic¶
TimeConsumer(
builder: (context, frame, _) {
// Different content at different frames
if (frame < 30) {
return Text('Loading...');
} else if (frame < 90) {
return Text('Content');
} else {
return Text('Finished');
}
},
)
Looping Animation¶
TimeConsumer(
builder: (context, frame, _) {
// 60-frame loop (2 seconds at 30fps)
final loopFrame = frame % 60;
final loopProgress = loopFrame / 60;
// Oscillate: 0 → 1 → 0
final oscillation = sin(loopProgress * 2 * pi);
return Transform.translate(
offset: Offset(0, oscillation * 20),
child: BouncingWidget(),
);
},
)
Frame vs Progress¶
Frame Number¶
The frame parameter is the absolute frame number from the start of the composition:
// At 30fps:
// frame 0 = 0.0 seconds
// frame 30 = 1.0 seconds
// frame 90 = 3.0 seconds
TimeConsumer(
builder: (context, frame, _) {
final seconds = frame / 30;
return Text('Time: ${seconds.toStringAsFixed(1)}s');
},
)
Progress Value¶
The progress parameter is normalized 0.0-1.0 through the entire composition:
// progress 0.0 = start of video
// progress 0.5 = middle of video
// progress 1.0 = end of video
TimeConsumer(
builder: (context, _, progress) {
final percentage = (progress * 100).round();
return Text('Progress: $percentage%');
},
)
Scene-Relative Frames¶
When inside a Scene, you often want frame numbers relative to the scene start:
Scene(
durationInFrames: 120,
children: [
TimeConsumer(
builder: (context, globalFrame, _) {
// Get scene context
final sceneContext = SceneContext.of(context);
// Calculate local frame (0 to 119 within this scene)
final localFrame = sceneContext != null
? globalFrame - sceneContext.sceneStartFrame
: globalFrame;
// Get scene progress (0.0 to 1.0 within this scene)
final sceneProgress = sceneContext?.progress ?? 0.0;
return AnimatedWidget(
localFrame: localFrame,
progress: sceneProgress,
);
},
),
],
)
Helper for Local Frames¶
Create a helper widget for common patterns:
class SceneTimeConsumer extends StatelessWidget {
final Widget Function(BuildContext, int localFrame, double sceneProgress) builder;
const SceneTimeConsumer({required this.builder});
@override
Widget build(BuildContext context) {
return TimeConsumer(
builder: (context, globalFrame, _) {
final sceneContext = SceneContext.of(context);
final localFrame = sceneContext != null
? globalFrame - sceneContext.sceneStartFrame
: globalFrame;
final sceneProgress = sceneContext?.progress ?? 0.0;
return builder(context, localFrame, sceneProgress);
},
);
}
}
// Usage
SceneTimeConsumer(
builder: (context, localFrame, progress) {
// localFrame is now 0 to sceneLength-1
return MyAnimatedWidget(frame: localFrame);
},
)
Animation Patterns¶
Delayed Start¶
TimeConsumer(
builder: (context, frame, _) {
final sceneContext = SceneContext.of(context);
final localFrame = frame - (sceneContext?.sceneStartFrame ?? 0);
// Start at frame 30, animate for 45 frames
const startFrame = 30;
const duration = 45;
final progress = ((localFrame - startFrame) / duration).clamp(0.0, 1.0);
final easedProgress = Curves.easeOutCubic.transform(progress);
return Opacity(
opacity: easedProgress,
child: Content(),
);
},
)
Multiple Phases¶
TimeConsumer(
builder: (context, frame, _) {
final sceneContext = SceneContext.of(context);
final localFrame = frame - (sceneContext?.sceneStartFrame ?? 0);
// Phase 1: Frames 0-30 - Fade in
// Phase 2: Frames 30-90 - Visible
// Phase 3: Frames 90-120 - Fade out
double opacity;
if (localFrame < 30) {
opacity = localFrame / 30;
} else if (localFrame < 90) {
opacity = 1.0;
} else {
opacity = 1.0 - ((localFrame - 90) / 30);
}
return Opacity(
opacity: opacity.clamp(0.0, 1.0),
child: Content(),
);
},
)
Keyframe Animation¶
TimeConsumer(
builder: (context, frame, _) {
final sceneContext = SceneContext.of(context);
final localFrame = frame - (sceneContext?.sceneStartFrame ?? 0);
// Use interpolate for keyframes
final scale = interpolate(
localFrame.toDouble(),
inputRange: [0, 15, 25, 30],
outputRange: [1.0, 1.3, 0.9, 1.0],
curve: Curves.easeInOut,
);
return Transform.scale(
scale: scale,
child: Content(),
);
},
)
Performance Tips¶
1. Minimize Rebuilds¶
Only rebuild what changes:
// Bad: Rebuilds entire complex widget tree every frame
TimeConsumer(
builder: (context, frame, progress) {
return ComplexWidget(
opacity: progress, // Only this changes
child: VeryExpensiveWidget(), // This doesn't change
);
},
)
// Good: Only rebuild the changing part
Stack(
children: [
const VeryExpensiveWidget(), // Never rebuilds
TimeConsumer(
builder: (context, frame, progress) {
return Opacity(
opacity: progress,
child: const SizedBox(), // Minimal widget
);
},
),
],
)
2. Cache Expensive Calculations¶
TimeConsumer(
builder: (context, frame, _) {
// Bad: Recalculates every frame
final expensiveValue = calculateExpensiveThing();
// Good: Only calculate when frame changes significantly
final keyFrame = frame ~/ 10; // Only recalc every 10 frames
return CachedBuilder(
key: keyFrame,
builder: () => calculateExpensiveThing(),
);
},
)
3. Use AnimatedProp for Simple Animations¶
// Instead of this:
TimeConsumer(
builder: (context, frame, _) {
final progress = ((frame - 30) / 45).clamp(0.0, 1.0);
return Opacity(
opacity: progress,
child: Text('Hello'),
);
},
)
// Use this:
AnimatedProp(
startFrame: 30,
duration: 45,
animation: PropAnimation.fadeIn(),
child: Text('Hello'),
)
Related¶
- AnimatedProp - Pre-built animations
- AnimatedText - Text-specific animations
- interpolate - Keyframe interpolation
- Frame-Based Animation - Concepts