Line data Source code
1 : import 'dart:ui';
2 :
3 : import 'assets/images.dart';
4 : import 'extensions/vector2.dart';
5 : import 'flame.dart';
6 : import 'sprite.dart';
7 :
8 : export 'sprite.dart';
9 :
10 : class SpriteAnimationFrameData {
11 : /// Coordinates of the sprite of this Frame
12 : final Vector2 srcPosition;
13 :
14 : /// Size of the sprite of this Frame
15 : final Vector2 srcSize;
16 :
17 : /// The duration to display it, in seconds.
18 : final double stepTime;
19 :
20 0 : SpriteAnimationFrameData({
21 : required this.srcPosition,
22 : required this.srcSize,
23 : required this.stepTime,
24 : });
25 : }
26 :
27 : class SpriteAnimationData {
28 : late List<SpriteAnimationFrameData> frames;
29 : final bool loop;
30 :
31 : /// Creates a SpriteAnimationData from the given [frames] and [loop] parameters
32 0 : SpriteAnimationData(this.frames, {this.loop = true});
33 :
34 : /// Takes some parameters and automatically calculate and create the frames for the sprite animation data
35 : ///
36 : /// [amount] The total amount of frames present on the image
37 : /// [stepTimes] A list of times (in seconds) of each frame, should have a length equals to the amount parameter
38 : /// [textureSize] The size of each frame
39 : /// [amountPerRow] An optional parameter to inform how many frames there are on which row, useful for sprite sheets where the frames as disposed on multiple lines
40 : /// [texturePosition] An optional parameter with the initial coordinate where the frames begin on the image, default to (top: 0, left: 0)
41 : /// [loop] An optional parameter to inform if this animation loops or has a single iteration, defaults to true
42 0 : SpriteAnimationData.variable({
43 : required int amount,
44 : required List<double> stepTimes,
45 : required Vector2 textureSize,
46 : int? amountPerRow,
47 : Vector2? texturePosition,
48 : this.loop = true,
49 0 : }) : assert(amountPerRow == null || amount >= amountPerRow) {
50 : amountPerRow ??= amount;
51 0 : texturePosition ??= Vector2.zero();
52 0 : frames = List<SpriteAnimationFrameData>.generate(amount, (i) {
53 0 : final position = Vector2(
54 0 : texturePosition!.x + (i % amountPerRow!) * textureSize.x,
55 0 : texturePosition.y + (i ~/ amountPerRow) * textureSize.y,
56 : );
57 0 : return SpriteAnimationFrameData(
58 0 : stepTime: stepTimes[i],
59 : srcPosition: position,
60 : srcSize: textureSize,
61 : );
62 : });
63 : }
64 :
65 : /// Works just like [SpriteAnimationData.variable] but uses the same [stepTime] for all frames
66 0 : factory SpriteAnimationData.sequenced({
67 : required int amount,
68 : required double stepTime,
69 : required Vector2 textureSize,
70 : int? amountPerRow,
71 : Vector2? texturePosition,
72 : bool loop = true,
73 : }) {
74 0 : return SpriteAnimationData.variable(
75 : amount: amount,
76 : amountPerRow: amountPerRow,
77 : texturePosition: texturePosition,
78 : textureSize: textureSize,
79 : loop: loop,
80 0 : stepTimes: List.filled(amount, stepTime),
81 : );
82 : }
83 : }
84 :
85 : /// Represents a single sprite animation frame.
86 : class SpriteAnimationFrame {
87 : /// The [Sprite] to be displayed.
88 : Sprite sprite;
89 :
90 : /// The duration to display it, in seconds.
91 : double stepTime;
92 :
93 : /// Create based on the parameters.
94 2 : SpriteAnimationFrame(this.sprite, this.stepTime);
95 : }
96 :
97 : typedef OnCompleteSpriteAnimation = void Function();
98 :
99 : /// Represents a sprite animation, that is, a list of sprites that change with time.
100 : class SpriteAnimation {
101 : /// The frames that compose this animation.
102 : List<SpriteAnimationFrame> frames = [];
103 :
104 : /// Index of the current frame that should be displayed.
105 : int currentIndex = 0;
106 :
107 : /// Current clock time (total time) of this animation, in seconds, since last frame.
108 : ///
109 : /// It's ticked by the update method. It's reset every frame change.
110 : double clock = 0.0;
111 :
112 : /// Total elapsed time of this animation, in seconds, since start or a reset.
113 : double elapsed = 0.0;
114 :
115 : /// Whether the animation loops after the last sprite of the list, going back to the first, or keeps returning the last when done.
116 : bool loop = true;
117 :
118 : /// Registered method to be triggered when the animation complete.
119 : OnCompleteSpriteAnimation? onComplete;
120 :
121 : /// Creates an animation given a list of frames.
122 0 : SpriteAnimation(this.frames, {this.loop = true});
123 :
124 : /// Creates an empty animation
125 0 : SpriteAnimation.empty();
126 :
127 : /// Creates an animation based on the parameters.
128 : ///
129 : /// All frames have the same [stepTime].
130 2 : SpriteAnimation.spriteList(
131 : List<Sprite> sprites, {
132 : required double stepTime,
133 : this.loop = true,
134 : }) {
135 2 : if (sprites.isEmpty) {
136 0 : throw Exception('You must have at least one frame!');
137 : }
138 10 : frames = sprites.map((s) => SpriteAnimationFrame(s, stepTime)).toList();
139 : }
140 :
141 : /// Creates an SpriteAnimation based on its [data].
142 : ///
143 : /// Check [SpriteAnimationData] constructors for more info.
144 0 : SpriteAnimation.fromFrameData(
145 : Image image,
146 : SpriteAnimationData data,
147 : ) {
148 0 : frames = data.frames.map((frameData) {
149 0 : return SpriteAnimationFrame(
150 0 : Sprite(
151 : image,
152 0 : srcSize: frameData.srcSize,
153 0 : srcPosition: frameData.srcPosition,
154 : ),
155 0 : frameData.stepTime,
156 : );
157 0 : }).toList();
158 0 : loop = data.loop;
159 : }
160 :
161 : /// Automatically creates an Animation Object using animation data provided by the json file
162 : /// provided by Aseprite
163 : ///
164 : /// [imagePath]: Source of the sprite sheet animation
165 : /// [dataPath]: Animation's exported data in json format
166 0 : SpriteAnimation.fromAsepriteData(
167 : Image image,
168 : Map<String, dynamic> jsonData,
169 : ) {
170 0 : final jsonFrames = jsonData['frames'] as Map<String, dynamic>;
171 :
172 0 : final frames = jsonFrames.values.map((dynamic value) {
173 : final map = value as Map;
174 0 : final frameData = map['frame'] as Map<String, dynamic>;
175 0 : final x = frameData['x'] as int;
176 0 : final y = frameData['y'] as int;
177 0 : final width = frameData['w'] as int;
178 0 : final height = frameData['h'] as int;
179 :
180 0 : final stepTime = (map['duration'] as int) / 1000;
181 :
182 0 : final sprite = Sprite(
183 : image,
184 0 : srcPosition: Vector2Extension.fromInts(x, y),
185 0 : srcSize: Vector2Extension.fromInts(width, height),
186 : );
187 :
188 0 : return SpriteAnimationFrame(sprite, stepTime);
189 : });
190 :
191 0 : this.frames = frames.toList();
192 0 : loop = true;
193 : }
194 :
195 : /// Takes a path of an image, a [SpriteAnimationData] and loads the sprite animation
196 : /// When the [images] is omitted, the global [Flame.images] is used
197 0 : static Future<SpriteAnimation> load(
198 : String src,
199 : SpriteAnimationData data, {
200 : Images? images,
201 : }) async {
202 0 : final _images = images ?? Flame.images;
203 0 : final image = await _images.load(src);
204 0 : return SpriteAnimation.fromFrameData(image, data);
205 : }
206 :
207 : /// The current frame that should be displayed.
208 8 : SpriteAnimationFrame get currentFrame => frames[currentIndex];
209 :
210 : /// Returns whether the animation is on the last frame.
211 12 : bool get isLastFrame => currentIndex == frames.length - 1;
212 :
213 : /// Returns whether the animation has only a single frame (and is, thus, a still image).
214 8 : bool get isSingleFrame => frames.length == 1;
215 :
216 : /// Sets a different step time to each frame. The sizes of the arrays must match.
217 0 : set variableStepTimes(List<double> stepTimes) {
218 0 : assert(stepTimes.length == frames.length);
219 0 : for (var i = 0; i < frames.length; i++) {
220 0 : frames[i].stepTime = stepTimes[i];
221 : }
222 : }
223 :
224 : /// Sets a fixed step time to all frames.
225 0 : set stepTime(double stepTime) {
226 0 : frames.forEach((frame) => frame.stepTime = stepTime);
227 : }
228 :
229 : /// Resets the animation, like it would just have been created.
230 1 : void reset() {
231 1 : clock = 0.0;
232 1 : elapsed = 0.0;
233 1 : currentIndex = 0;
234 1 : _done = false;
235 : }
236 :
237 : /// Gets the current [Sprite] that should be shown.
238 : ///
239 : /// In case it reaches the end:
240 : /// * If [loop] is true, it will return the last sprite. Otherwise, it will go back to the first.
241 0 : Sprite getSprite() {
242 0 : return currentFrame.sprite;
243 : }
244 :
245 : /// If [loop] is false, returns whether the animation is done (fixed in the last Sprite).
246 : ///
247 : /// Always returns false otherwise.
248 : bool _done = false;
249 4 : bool done() => _done;
250 :
251 : /// Updates this animation, ticking the lifeTime by an amount [dt] (in seconds).
252 2 : void update(double dt) {
253 4 : clock += dt;
254 4 : elapsed += dt;
255 4 : if (isSingleFrame || _done) {
256 : return;
257 : }
258 8 : while (clock >= currentFrame.stepTime) {
259 2 : if (isLastFrame) {
260 2 : if (loop) {
261 8 : clock -= currentFrame.stepTime;
262 2 : currentIndex = 0;
263 : } else {
264 2 : _done = true;
265 3 : onComplete?.call();
266 : return;
267 : }
268 : } else {
269 8 : clock -= currentFrame.stepTime;
270 4 : currentIndex++;
271 : }
272 : }
273 : }
274 :
275 : /// Returns a new Animation based on this animation, but with its frames in reversed order
276 0 : SpriteAnimation reversed() {
277 0 : return SpriteAnimation(frames.reversed.toList(), loop: loop);
278 : }
279 :
280 : /// Computes the total duration of this animation (before it's done or repeats).
281 0 : double totalDuration() {
282 0 : return frames.map((f) => f.stepTime).reduce((a, b) => a + b);
283 : }
284 : }
|