Line data Source code
1 : import 'dart:async';
2 : import 'dart:ui';
3 :
4 : import 'package:flutter/painting.dart';
5 :
6 : import 'assets/images.dart';
7 : import 'extensions/canvas.dart';
8 : import 'extensions/image.dart';
9 : import 'extensions/rect.dart';
10 : import 'extensions/vector2.dart';
11 : import 'flame.dart';
12 : import 'game/game.dart';
13 : import 'sprite_animation.dart';
14 :
15 : extension ParallaxExtension on Game {
16 0 : Future<Parallax> loadParallax(
17 : List<ParallaxData> dataList, {
18 : Vector2? size,
19 : Vector2? baseVelocity,
20 : Vector2? velocityMultiplierDelta,
21 : ImageRepeat repeat = ImageRepeat.repeatX,
22 : Alignment alignment = Alignment.bottomLeft,
23 : LayerFill fill = LayerFill.height,
24 : }) {
25 0 : return Parallax.load(
26 : dataList,
27 : size: size,
28 : baseVelocity: baseVelocity,
29 : velocityMultiplierDelta: velocityMultiplierDelta,
30 : repeat: repeat,
31 : alignment: alignment,
32 : fill: fill,
33 0 : images: images,
34 : );
35 : }
36 :
37 0 : Future<ParallaxImage> loadParallaxImage(
38 : String path, {
39 : ImageRepeat repeat = ImageRepeat.repeatX,
40 : Alignment alignment = Alignment.bottomLeft,
41 : LayerFill fill = LayerFill.height,
42 : }) {
43 0 : return ParallaxImage.load(
44 : path,
45 : repeat: repeat,
46 : alignment: alignment,
47 : fill: fill,
48 0 : images: images,
49 : );
50 : }
51 :
52 0 : Future<ParallaxAnimation> loadParallaxAnimation(
53 : String path,
54 : SpriteAnimationData animaitonData, {
55 : ImageRepeat repeat = ImageRepeat.repeatX,
56 : Alignment alignment = Alignment.bottomLeft,
57 : LayerFill fill = LayerFill.height,
58 : }) {
59 0 : return ParallaxAnimation.load(
60 : path,
61 : animaitonData,
62 : repeat: repeat,
63 : alignment: alignment,
64 : fill: fill,
65 0 : images: images,
66 : );
67 : }
68 :
69 0 : Future<ParallaxLayer> loadParallaxLayer(
70 : ParallaxData data, {
71 : ImageRepeat repeat = ImageRepeat.repeatX,
72 : Alignment alignment = Alignment.bottomLeft,
73 : LayerFill fill = LayerFill.height,
74 : Vector2? velocityMultiplier,
75 : }) {
76 0 : return ParallaxLayer.load(
77 : data,
78 : velocityMultiplier: velocityMultiplier,
79 : repeat: repeat,
80 : alignment: alignment,
81 : fill: fill,
82 0 : images: images,
83 : );
84 : }
85 : }
86 :
87 : abstract class ParallaxRenderer {
88 : /// If and how the image should be repeated on the canvas
89 : final ImageRepeat repeat;
90 :
91 : /// How to align the image in relation to the screen
92 : final Alignment alignment;
93 :
94 : /// How to fill the screen with the image, always proportionally scaled.
95 : final LayerFill fill;
96 :
97 0 : ParallaxRenderer({
98 : ImageRepeat? repeat,
99 : Alignment? alignment,
100 : LayerFill? fill,
101 : }) : repeat = repeat ?? ImageRepeat.repeatX,
102 : alignment = alignment ?? Alignment.bottomLeft,
103 : fill = fill ?? LayerFill.height;
104 :
105 : void update(double dt);
106 : Image get image;
107 : }
108 :
109 : /// Specifications with a path to an image and how it should be drawn in
110 : /// relation to the device screen
111 : class ParallaxImage extends ParallaxRenderer {
112 : /// The image
113 : final Image _image;
114 :
115 0 : ParallaxImage(
116 : this._image, {
117 : ImageRepeat? repeat,
118 : Alignment? alignment,
119 : LayerFill? fill,
120 0 : }) : super(
121 : repeat: repeat,
122 : alignment: alignment,
123 : fill: fill,
124 : );
125 :
126 : /// Takes a path of an image, and optionally arguments for how the image should
127 : /// repeat ([repeat]), which edge it should align with ([alignment]), which axis
128 : /// it should fill the image on ([fill]) and [images] which is the image cache
129 : /// that should be used. If no image cache is set, the global flame cache is used.
130 0 : static Future<ParallaxImage> load(
131 : String path, {
132 : ImageRepeat repeat = ImageRepeat.repeatX,
133 : Alignment alignment = Alignment.bottomLeft,
134 : LayerFill fill = LayerFill.height,
135 : Images? images,
136 : }) async {
137 0 : images ??= Flame.images;
138 0 : return ParallaxImage(
139 0 : await images.load(path),
140 : repeat: repeat,
141 : alignment: alignment,
142 : fill: fill,
143 : );
144 : }
145 :
146 0 : @override
147 0 : Image get image => _image;
148 :
149 0 : @override
150 : void update(_) {
151 : // noop
152 : }
153 : }
154 :
155 : /// Specifications with a SpriteAnimation and how it should be drawn in
156 : /// relation to the device screen
157 : class ParallaxAnimation extends ParallaxRenderer {
158 : /// The Animation
159 : final SpriteAnimation _animation;
160 :
161 : /// The animation's frames prerended into images so it can be used in the parallax
162 : final List<Image> _prerenderedFrames;
163 :
164 0 : ParallaxAnimation(
165 : this._animation,
166 : this._prerenderedFrames, {
167 : ImageRepeat? repeat,
168 : Alignment? alignment,
169 : LayerFill? fill,
170 0 : }) : super(
171 : repeat: repeat,
172 : alignment: alignment,
173 : fill: fill,
174 : );
175 :
176 : /// Takes a path of an image, a SpriteAnimationData, and optionally arguments for how the image should
177 : /// repeat ([repeat]), which edge it should align with ([alignment]), which axis
178 : /// it should fill the image on ([fill]) and [images] which is the image cache
179 : /// that should be used. If no image cache is set, the global flame cache is used.
180 : ///
181 : /// _IMPORTANT_: This method pre render all the frames of the animation into image instances
182 : /// so it can be used inside the parallax. Just keep that in mind when using animations in
183 : /// in parallax, the over use of it, or the use of big animations (be it in number of frames
184 : /// or the size of the images) can lead to high use of memory.
185 0 : static Future<ParallaxAnimation> load(
186 : String path,
187 : SpriteAnimationData animationData, {
188 : ImageRepeat repeat = ImageRepeat.repeatX,
189 : Alignment alignment = Alignment.bottomLeft,
190 : LayerFill fill = LayerFill.height,
191 : Images? images,
192 : }) async {
193 0 : images ??= Flame.images;
194 :
195 : final animation =
196 0 : await SpriteAnimation.load(path, animationData, images: images);
197 0 : final prerendedFrames = await Future.wait(
198 0 : animation.frames.map((frame) => frame.sprite.toImage()).toList(),
199 : );
200 :
201 0 : return ParallaxAnimation(
202 : animation,
203 : prerendedFrames,
204 : repeat: repeat,
205 : alignment: alignment,
206 : fill: fill,
207 : );
208 : }
209 :
210 0 : @override
211 0 : Image get image => _prerenderedFrames[_animation.currentIndex];
212 :
213 0 : @override
214 : void update(double dt) {
215 0 : _animation.update(dt);
216 : }
217 : }
218 :
219 : /// Represents one layer in the parallax, draws out an image on a canvas in the
220 : /// manner specified by the parallaxImage
221 : class ParallaxLayer {
222 : final ParallaxRenderer parallaxRenderer;
223 : late Vector2 velocityMultiplier;
224 : late Rect _paintArea;
225 : late Vector2 _scroll;
226 : late Vector2 _imageSize;
227 : double _scale = 1.0;
228 :
229 : /// [parallaxRenderer] is the representation of the renderer with data of how the
230 : /// layer should behave.
231 : /// [velocityMultiplier] will be used to determine the velocity of the layer by
232 : /// multiplying the [Parallax.baseVelocity] with the [velocityMultiplier].
233 0 : ParallaxLayer(
234 : this.parallaxRenderer, {
235 : Vector2? velocityMultiplier,
236 0 : }) : velocityMultiplier = velocityMultiplier ?? Vector2.all(1.0);
237 :
238 0 : Vector2 currentOffset() => _scroll;
239 :
240 0 : void resize(Vector2 size) {
241 0 : double scale(LayerFill fill) {
242 : switch (fill) {
243 0 : case LayerFill.height:
244 0 : return parallaxRenderer.image.height / size.y;
245 0 : case LayerFill.width:
246 0 : return parallaxRenderer.image.width / size.x;
247 : default:
248 0 : return _scale;
249 : }
250 : }
251 :
252 0 : _scale = scale(parallaxRenderer.fill);
253 :
254 : // The image size so that it fulfills the LayerFill parameter
255 0 : _imageSize = parallaxRenderer.image.size / _scale;
256 :
257 : // Number of images that can fit on the canvas plus one
258 : // to have something to scroll to without leaving canvas empty
259 0 : final count = Vector2.all(1) + (size.clone()..divide(_imageSize));
260 :
261 : // Percentage of the image size that will overflow
262 0 : final overflow = ((_imageSize.clone()..multiply(count)) - size)
263 0 : ..divide(_imageSize);
264 :
265 : // Align image to correct side of the screen
266 0 : final alignment = parallaxRenderer.alignment;
267 :
268 0 : final marginX = alignment.x * overflow.x / 2 + overflow.x / 2;
269 0 : final marginY = alignment.y * overflow.y / 2 + overflow.y / 2;
270 :
271 0 : _scroll = Vector2(marginX, marginY);
272 :
273 : // Size of the area to paint the images on
274 0 : final paintSize = count..multiply(_imageSize);
275 0 : _paintArea = paintSize.toRect();
276 : }
277 :
278 0 : void update(Vector2 delta, double dt) {
279 0 : parallaxRenderer.update(dt);
280 : // Scale the delta so that images that are larger don't scroll faster
281 0 : _scroll += delta.clone()..divide(_imageSize);
282 0 : switch (parallaxRenderer.repeat) {
283 0 : case ImageRepeat.repeat:
284 0 : _scroll = Vector2(_scroll.x % 1, _scroll.y % 1);
285 : break;
286 0 : case ImageRepeat.repeatX:
287 0 : _scroll = Vector2(_scroll.x % 1, _scroll.y);
288 : break;
289 0 : case ImageRepeat.repeatY:
290 0 : _scroll = Vector2(_scroll.x, _scroll.y % 1);
291 : break;
292 0 : case ImageRepeat.noRepeat:
293 : break;
294 : }
295 :
296 0 : final scrollPosition = _scroll.clone()..multiply(_imageSize);
297 0 : _paintArea = Rect.fromLTWH(
298 0 : -scrollPosition.x,
299 0 : -scrollPosition.y,
300 0 : _paintArea.width,
301 0 : _paintArea.height,
302 : );
303 : }
304 :
305 0 : void render(Canvas canvas) {
306 0 : if (_paintArea.isEmpty) {
307 : return;
308 : }
309 0 : paintImage(
310 : canvas: canvas,
311 0 : image: parallaxRenderer.image,
312 0 : rect: _paintArea,
313 0 : repeat: parallaxRenderer.repeat,
314 0 : scale: _scale,
315 0 : alignment: parallaxRenderer.alignment,
316 : );
317 : }
318 :
319 : /// Takes a data of a parallax renderer, and optionally arguments for how it should
320 : /// repeat ([repeat]), which edge it should align with ([alignment]), which axis
321 : /// it should fill the image on ([fill]) and [images] which is the image cache
322 : /// that should be used. If no image cache is set, the global flame cache is used.
323 0 : static Future<ParallaxLayer> load(
324 : ParallaxData data, {
325 : Vector2? velocityMultiplier,
326 : ImageRepeat repeat = ImageRepeat.repeatX,
327 : Alignment alignment = Alignment.bottomLeft,
328 : LayerFill fill = LayerFill.height,
329 : Images? images,
330 : }) async {
331 0 : return ParallaxLayer(
332 0 : await data.load(
333 : repeat,
334 : alignment,
335 : fill,
336 : images,
337 : ),
338 : velocityMultiplier: velocityMultiplier,
339 : );
340 : }
341 : }
342 :
343 : /// How to fill the screen with the image, always proportionally scaled.
344 41 : enum LayerFill { height, width, none }
345 :
346 : abstract class ParallaxData {
347 : Future<ParallaxRenderer> load(
348 : ImageRepeat repeat,
349 : Alignment alignment,
350 : LayerFill fill,
351 : Images? images,
352 : );
353 : }
354 :
355 : /// Contains the fields and logic to load a [ParallaxImage]
356 : class ParallaxImageData extends ParallaxData {
357 : final String path;
358 :
359 0 : ParallaxImageData(this.path);
360 :
361 0 : @override
362 : Future<ParallaxRenderer> load(
363 : ImageRepeat repeat,
364 : Alignment alignment,
365 : LayerFill fill,
366 : Images? images,
367 : ) {
368 0 : return ParallaxImage.load(
369 0 : path,
370 : repeat: repeat,
371 : alignment: alignment,
372 : fill: fill,
373 : images: images,
374 : );
375 : }
376 : }
377 :
378 : /// Contains the fields and logic to load a [ParallaxAnimation]
379 : class ParallaxAnimationData extends ParallaxData {
380 : final String path;
381 : final SpriteAnimationData animationData;
382 :
383 0 : ParallaxAnimationData(this.path, this.animationData);
384 :
385 0 : @override
386 : Future<ParallaxRenderer> load(
387 : ImageRepeat repeat,
388 : Alignment alignment,
389 : LayerFill fill,
390 : Images? images,
391 : ) {
392 0 : return ParallaxAnimation.load(
393 0 : path,
394 0 : animationData,
395 : repeat: repeat,
396 : alignment: alignment,
397 : fill: fill,
398 : images: images,
399 : );
400 : }
401 : }
402 :
403 : /// A full parallax, several layers of images drawn out on the screen and each
404 : /// layer moves with different velocities to give an effect of depth.
405 : class Parallax {
406 : late Vector2 baseVelocity;
407 : late Rect _clipRect;
408 : final List<ParallaxLayer> layers;
409 :
410 : bool isSized = false;
411 : late final Vector2 _size;
412 :
413 : /// Do not modify this directly, since the layers won't be resized if you do
414 2 : Vector2 get size => _size;
415 0 : set size(Vector2 newSize) {
416 0 : resize(newSize);
417 : }
418 :
419 1 : Parallax(
420 : this.layers, {
421 : Vector2? size,
422 : Vector2? baseVelocity,
423 : }) {
424 1 : this.baseVelocity = baseVelocity ?? Vector2.zero();
425 : if (size != null) {
426 1 : resize(size);
427 : }
428 : }
429 :
430 : /// The base offset of the parallax, can be used in an outer update loop
431 : /// if you want to transition the parallax to a certain position.
432 0 : Vector2 currentOffset() => layers[0].currentOffset();
433 :
434 : /// If the `ParallaxComponent` isn't used your own wrapper needs to call this
435 : /// on creation.
436 1 : void resize(Vector2 newSize) {
437 1 : if (!isSized) {
438 2 : _size = Vector2.zero();
439 : }
440 3 : if (newSize != _size || !isSized) {
441 2 : _size.setFrom(newSize);
442 3 : _clipRect = _size.toRect();
443 2 : layers.forEach((layer) => layer.resize(_size));
444 : }
445 2 : isSized |= true;
446 : }
447 :
448 1 : void update(double dt) {
449 2 : layers.forEach((layer) {
450 0 : layer.update(
451 0 : (baseVelocity.clone()..multiply(layer.velocityMultiplier)) * dt,
452 : dt,
453 : );
454 : });
455 : }
456 :
457 : /// Note that this method only should be used if all of your layers should
458 : /// have the same layer arguments (how the images should be repeated, aligned
459 : /// and filled), otherwise load the [ParallaxLayer]s individually and use the
460 : /// normal constructor.
461 : ///
462 : /// [load] takes a list of paths to all the images that you want to use in the
463 : /// parallax.
464 : /// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta] can be passed
465 : /// in, [baseVelocity] defines what the base velocity of the layers should be
466 : /// and [velocityMultiplierDelta] defines how the velocity should change the
467 : /// closer the layer is ([velocityMultiplierDelta ^ n], where n is the
468 : /// layer index).
469 : /// Arguments for how all the images should repeat ([repeat]),
470 : /// which edge it should align with ([alignment]), which axis it should fill
471 : /// the image on ([fill]) and [images] which is the image cache that should be
472 : /// used can also be passed in.
473 : /// If no image cache is set, the global flame cache is used.
474 1 : static Future<Parallax> load(
475 : List<ParallaxData> dataList, {
476 : Vector2? size,
477 : Vector2? baseVelocity,
478 : Vector2? velocityMultiplierDelta,
479 : ImageRepeat repeat = ImageRepeat.repeatX,
480 : Alignment alignment = Alignment.bottomLeft,
481 : LayerFill fill = LayerFill.height,
482 : Images? images,
483 : }) async {
484 0 : final velocityDelta = velocityMultiplierDelta ?? Vector2.all(1.0);
485 : var depth = 0;
486 2 : final layers = await Future.wait<ParallaxLayer>(
487 1 : dataList.map((data) async {
488 0 : final renderer = await data.load(
489 : repeat,
490 : alignment,
491 : fill,
492 : images,
493 : );
494 : final velocityMultiplier =
495 0 : List.filled(depth, velocityDelta).fold<Vector2>(
496 : velocityDelta,
497 0 : (previousValue, delta) => previousValue.clone()..multiply(delta),
498 : );
499 0 : ++depth;
500 0 : return ParallaxLayer(
501 : renderer,
502 : velocityMultiplier: velocityMultiplier,
503 : );
504 : }),
505 : );
506 1 : return Parallax(
507 : layers,
508 : size: size,
509 : baseVelocity: baseVelocity,
510 : );
511 : }
512 :
513 0 : void render(Canvas canvas, {Vector2? position}) {
514 0 : canvas.save();
515 : if (position != null) {
516 0 : canvas.translateVector(position);
517 : }
518 0 : canvas.clipRect(_clipRect);
519 0 : layers.forEach((layer) {
520 0 : canvas.save();
521 0 : layer.render(canvas);
522 0 : canvas.restore();
523 : });
524 0 : canvas.restore();
525 : }
526 : }
|