Clips and crossfades: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: clips (line 40), player (line 49), live (line 125).
1/// Two clips on one cube: `AnimationPlayer` plays one, fades to the other and
2/// answers to a speed and a wrap mode while it does.
3///
4/// Quoted by `clip_playback.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
8import 'dart:typed_data';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class ClipPlaybackDemo extends ShowcaseDemo {
15 int wrapIndex = 0;
17 late final AnimationPlayer _player;
19 static const List<AnimationWrap> _wraps = <AnimationWrap>[
20 AnimationWrap.loop,
21 AnimationWrap.once,
22 AnimationWrap.pingPong,
23 ];
26 void configureView(DemoContext context) {
27 context.orbit
28 ..distance = 3.5
29 ..pitch = 0.25;
30 }
33 Scene build(DemoContext context) {
34 final MeshNode cube = MeshNode(
35 DeviceMesh.upload(context.device, CuboidShape().build()),
36 Material(name: 'cube', baseColor: Vector4(0.3, 0.55, 0.9, 1.0)),
37 name: 'cube',
38 );
40 final AnimationClip spin = AnimationClip(
41 name: 'spin',
42 tracks: <AnimationTrack>[_yawTrack(turns: 1, duration: 2.0)],
43 );
44 final AnimationClip nod = AnimationClip(
45 name: 'nod',
46 tracks: <AnimationTrack>[_nodTrack(duration: 1.2)],
47 );
49 _player = AnimationPlayer(
50 clips: <AnimationClip>[spin, nod],
51 targets: <AnimationTarget?>[cube],
52 );
53 _player.play(0);
54 _player.crossFadeTo(1, duration: 0.4);
56 return Scene()
57 ..add(cube)
58 ..add(
59 LightNode(name: 'sun', intensity: 3.0)
60 ..setLocalForward(Vector3(-0.4, -0.7, -0.5)),
61 );
62 }
64 /// A full turn about Y, five keys so the loop closes exactly on itself.
65 static AnimationTrack _yawTrack({
66 required int turns,
67 required double duration,
68 }) {
69 const int keys = 5;
70 final Float32List times = Float32List(keys);
71 final Float32List values = Float32List(keys * 4);
72 for (var i = 0; i < keys; i++) {
73 final double t = i / (keys - 1);
74 times[i] = t * duration;
75 final Quaternion q = Quaternion.axisAngle(
76 Vector3(0.0, 1.0, 0.0),
77 t * turns * 2 * math.pi,
78 );
79 values[i * 4] = q.x;
80 values[i * 4 + 1] = q.y;
81 values[i * 4 + 2] = q.z;
82 values[i * 4 + 3] = q.w;
83 }
84 return AnimationTrack(
85 nodeIndex: 0,
86 path: AnimationPath.rotation,
87 interpolation: AnimationInterpolation.linear,
88 times: times,
89 values: values,
90 componentCount: 4,
91 );
92 }
94 /// A short dip on X: level, down, level.
95 static AnimationTrack _nodTrack({required double duration}) {
96 final List<double> angles = <double>[0.0, -0.5, 0.0];
97 final Float32List times = Float32List.fromList(<double>[
98 0.0,
99 duration / 2,
100 duration,
101 ]);
102 final Float32List values = Float32List(angles.length * 4);
103 for (var i = 0; i < angles.length; i++) {
104 final Quaternion q = Quaternion.axisAngle(
105 Vector3(1.0, 0.0, 0.0),
106 angles[i],
107 );
108 values[i * 4] = q.x;
109 values[i * 4 + 1] = q.y;
110 values[i * 4 + 2] = q.z;
111 values[i * 4 + 3] = q.w;
112 }
113 return AnimationTrack(
114 nodeIndex: 0,
115 path: AnimationPath.rotation,
116 interpolation: AnimationInterpolation.linear,
117 times: times,
118 values: values,
119 componentCount: 4,
120 );
121 }
124 void update(DemoContext context, double dt) {
125 _player.update(dt);
126 }
129 List<DemoControl> controls(DemoContext context) => <DemoControl>[
130 SliderControl(
131 'Speed',
132 min: 0.1,
133 max: 3,
134 value: () => _player.speed,
135 onChanged: (double v) => _player.speed = v,
136 ),
137 ChoiceControl(
138 'Wrap',
139 options: const <String>['Loop', 'Once', 'Ping-pong'],
140 index: () => wrapIndex,
141 onChanged: (int i) {
142 wrapIndex = i;
143 _player.wrap = _wraps[i];
144 },
145 ),
146 ];
149 void verify(Scene scene, FrameResult frame) {
150 if (!_player.isPlaying) {
151 throw StateError('the player is not playing a clip');
152 }
153 if (!_player.isCrossFading) {
154 throw StateError('the crossfade to the second clip did not start');
155 }
156 final double weight = _player.fadeWeight;
157 if (weight <= 0.0 || weight >= 1.0) {
158 throw StateError('the crossfade is not partway through, got $weight');
159 }
160 }
161}