Adding animation support to parallax (#835)

* Adding animation support to parallax

* Solving workaround

* Fixing image composition add assert

* adding docs, linting and a better example

* lint

* Apply suggestions from code review

Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net>

* Update examples/lib/stories/parallax/sandbox_layer.dart

* Update doc/components.md

* Update examples/lib/stories/parallax/sandbox_layer.dart

Co-authored-by: Luan Nico <luanpotter27@gmail.com>

* Update examples/lib/stories/parallax/animation.dart

* formating

* Update .min_coverage

Co-authored-by: Jochum van der Ploeg <jochum@vdploeg.net>
Co-authored-by: Luan Nico <luanpotter27@gmail.com>
This commit is contained in:
Erick
2021-06-07 13:31:19 -03:00
committed by GitHub
parent 31f0437a40
commit 55aea41788
20 changed files with 472 additions and 84 deletions

View File

@ -276,8 +276,9 @@ For a working example, check the example in the
## ParallaxComponent ## ParallaxComponent
This Component can be used to render backgrounds with a depth feeling by drawing several transparent This `Component` can be used to render backgrounds with a depth feeling by drawing several transparent
images on top of each other, where each image is moving with a different velocity. images on top of each other, where each image or animation (`ParallaxRenderer`) is moving with a
different velocity.
The rationale is that when you look at the horizon and moving, closer objects seem to move faster The rationale is that when you look at the horizon and moving, closer objects seem to move faster
than distant ones. than distant ones.
@ -289,7 +290,10 @@ The simplest `ParallaxComponent` is created like this:
```dart ```dart
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final parallaxComponent = await loadParallaxComponent(['bg.png', 'trees.png']); final parallaxComponent = await loadParallaxComponent([
ParallaxImageData('bg.png'),
ParallaxImageData('trees.png'),
]);
add(parallax); add(parallax);
} }
``` ```
@ -300,7 +304,10 @@ A ParallaxComponent can also "load itself" by implementing the `onLoad` method:
class MyParallaxComponent extends ParallaxComponent with HasGameRef<MyGame> { class MyParallaxComponent extends ParallaxComponent with HasGameRef<MyGame> {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
parallax = await gameRef.loadParallax(['bg.png', 'trees.png']); parallax = await gameRef.loadParallax([
ParallaxImageData('bg.png'),
ParallaxImageData('trees.png'),
]);
} }
} }
@ -323,7 +330,7 @@ For example if you want to move your background images along the X-axis with a f
```dart ```dart
final parallaxComponent = await loadParallaxComponent( final parallaxComponent = await loadParallaxComponent(
_paths, _dataList,
baseVelocity: Vector2(20, 0), baseVelocity: Vector2(20, 0),
velocityMultiplierDelta: Vector2(1.8, 1.0), velocityMultiplierDelta: Vector2(1.8, 1.0),
); );
@ -341,7 +348,7 @@ parallax.velocityMultiplierDelta = Vector2(2.0, 1.0);
By default the images are aligned to the bottom left, repeated along the X-axis and scaled By default the images are aligned to the bottom left, repeated along the X-axis and scaled
proportionally so that the image covers the height of the screen. If you want to change this proportionally so that the image covers the height of the screen. If you want to change this
behavior, for example if you are not making a side scrolling game, you can set the `repeat`, behavior, for example if you are not making a side scrolling game, you can set the `repeat`,
`alignment` and `fill` parameters for each `ParallaxImage` and add them to `ParallaxLayer`s that you `alignment` and `fill` parameters for each `ParallaxRenderer` and add them to `ParallaxLayer`s that you
then pass in to the `ParallaxComponent`'s constructor. then pass in to the `ParallaxComponent`'s constructor.
Advanced example: Advanced example:
@ -372,12 +379,15 @@ component (`game.add(parallaxComponent`).
Also, don't forget to add you images to the `pubspec.yaml` file as assets or they wont be found. Also, don't forget to add you images to the `pubspec.yaml` file as assets or they wont be found.
The `Parallax` file contains an extension of the game which adds `loadParallax`, `loadParallaxLayer` The `Parallax` file contains an extension of the game which adds `loadParallax`, `loadParallaxLayer`
and `loadParallaxImage` so that it automatically uses your game's image cache instead of the global , `loadParallaxImage` and `loadParallaxAnimation` so that it automatically uses your game's image cache instead of the global
one. The same goes for the `ParallaxComponent` file, but that provides `loadParallaxComponent`. one. The same goes for the `ParallaxComponent` file, but that provides `loadParallaxComponent`.
If you want a fullscreen `ParallaxComponent` simply omit the `size` argument and it will take the If you want a fullscreen `ParallaxComponent` simply omit the `size` argument and it will take the
size of the game, it will also resize to fullscreen when the game changes size or orientation. size of the game, it will also resize to fullscreen when the game changes size or orientation.
Flame provides two kinds of `ParallaxRenderer`: `ParallaxImage` and `ParallaxAnimation`, `ParallaxImage` is a static image renderer and `ParallaxAnimation` is, as it's name implies, an animation and frame based renderer.
It is also possible to create custom renderers by extending the `ParallaxRenderer` class.
Three example implementations can be found in the Three example implementations can be found in the
[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax). [examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax).

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -15,7 +15,7 @@ class AdvancedParallaxGame extends BaseGame {
Future<void> onLoad() async { Future<void> onLoad() async {
final layers = _layersMeta.entries.map( final layers = _layersMeta.entries.map(
(e) => loadParallaxLayer( (e) => loadParallaxLayer(
e.key, ParallaxImageData(e.key),
velocityMultiplier: Vector2(e.value, 1.0), velocityMultiplier: Vector2(e.value, 1.0),
), ),
); );

View File

@ -0,0 +1,44 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
import 'package:flutter/painting.dart';
class AnimationParallaxGame extends BaseGame {
@override
Future<void> onLoad() async {
final cityLayer = await loadParallaxLayer(
ParallaxImageData('parallax/city.png'),
);
final rainLayer = await loadParallaxLayer(
ParallaxAnimationData(
'parallax/rain.png',
SpriteAnimationData.sequenced(
amount: 4,
stepTime: 0.3,
textureSize: Vector2(80, 160),
),
),
velocityMultiplier: Vector2(2, 0),
);
final cloudsLayer = await loadParallaxLayer(
ParallaxImageData('parallax/heavy_clouded.png'),
velocityMultiplier: Vector2(4, 0),
fill: LayerFill.none,
alignment: Alignment.topLeft,
);
final parallax = Parallax(
[
cityLayer,
rainLayer,
cloudsLayer,
],
baseVelocity: Vector2(20, 0),
);
final parallaxComponent = ParallaxComponent.fromParallax(parallax);
add(parallaxComponent);
}
}

View File

@ -1,13 +1,14 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
class BasicParallaxGame extends BaseGame { class BasicParallaxGame extends BaseGame {
final _imageNames = [ final _imageNames = [
'parallax/bg.png', ParallaxImageData('parallax/bg.png'),
'parallax/mountain-far.png', ParallaxImageData('parallax/mountain-far.png'),
'parallax/mountains.png', ParallaxImageData('parallax/mountains.png'),
'parallax/trees.png', ParallaxImageData('parallax/trees.png'),
'parallax/foreground-trees.png', ParallaxImageData('parallax/foreground-trees.png'),
]; ];
@override @override

View File

@ -15,11 +15,11 @@ class MyParallaxComponent extends ParallaxComponent
Future<void> onLoad() async { Future<void> onLoad() async {
parallax = await gameRef.loadParallax( parallax = await gameRef.loadParallax(
[ [
'parallax/bg.png', ParallaxImageData('parallax/bg.png'),
'parallax/mountain-far.png', ParallaxImageData('parallax/mountain-far.png'),
'parallax/mountains.png', ParallaxImageData('parallax/mountains.png'),
'parallax/trees.png', ParallaxImageData('parallax/trees.png'),
'parallax/foreground-trees.png', ParallaxImageData('parallax/foreground-trees.png'),
], ],
baseVelocity: Vector2(20, 0), baseVelocity: Vector2(20, 0),
velocityMultiplierDelta: Vector2(1.8, 1.0), velocityMultiplierDelta: Vector2(1.8, 1.0),

View File

@ -16,11 +16,11 @@ class NoFCSParallaxGame extends Game {
Future<void> onLoad() async { Future<void> onLoad() async {
parallax = await loadParallax( parallax = await loadParallax(
[ [
'parallax/bg.png', ParallaxImageData('parallax/bg.png'),
'parallax/mountain-far.png', ParallaxImageData('parallax/mountain-far.png'),
'parallax/mountains.png', ParallaxImageData('parallax/mountains.png'),
'parallax/trees.png', ParallaxImageData('parallax/trees.png'),
'parallax/foreground-trees.png', ParallaxImageData('parallax/foreground-trees.png'),
], ],
size: size, size: size,
baseVelocity: Vector2(20, 0), baseVelocity: Vector2(20, 0),

View File

@ -1,11 +1,15 @@
import 'package:dashbook/dashbook.dart'; import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
import 'package:flutter/painting.dart';
import '../../commons/commons.dart'; import '../../commons/commons.dart';
import 'advanced.dart'; import 'advanced.dart';
import 'animation.dart';
import 'basic.dart'; import 'basic.dart';
import 'component.dart'; import 'component.dart';
import 'no_fcs.dart'; import 'no_fcs.dart';
import 'sandbox_layer.dart';
import 'small_parallax.dart'; import 'small_parallax.dart';
void addParallaxStories(Dashbook dashbook) { void addParallaxStories(Dashbook dashbook) {
@ -23,6 +27,12 @@ void addParallaxStories(Dashbook dashbook) {
info: 'Shows how to do initiation and loading of assets from within an ' info: 'Shows how to do initiation and loading of assets from within an '
'extended ParallaxComponent', 'extended ParallaxComponent',
) )
..add(
'Animation',
(_) => GameWidget(game: AnimationParallaxGame()),
codeLink: baseLink('parallax/animation.dart'),
info: 'Shows how to use animations in a parallax',
)
..add( ..add(
'Non-fullscreen', 'Non-fullscreen',
(_) => GameWidget(game: SmallParallaxGame()), (_) => GameWidget(game: SmallParallaxGame()),
@ -42,5 +52,45 @@ void addParallaxStories(Dashbook dashbook) {
codeLink: baseLink('parallax/advanced.dart'), codeLink: baseLink('parallax/advanced.dart'),
info: 'Shows how to create a parallax with different velocity deltas on ' info: 'Shows how to create a parallax with different velocity deltas on '
'each layer', 'each layer',
)
..add(
'Layer sandbox',
(context) {
return GameWidget(
game: SandBoxLayerParallaxGame(
planeSpeed: Vector2(
context.numberProperty('plane x speed', 0),
context.numberProperty('plane y speed', 0),
),
planeRepeat: context.listProperty(
'plane repeat strategy',
ImageRepeat.noRepeat,
ImageRepeat.values,
),
planeFill: context.listProperty(
'plane fill strategy',
LayerFill.none,
LayerFill.values,
),
planeAlignment: context.listProperty(
'plane alignment strategy',
Alignment.center,
[
Alignment.topLeft,
Alignment.topRight,
Alignment.center,
Alignment.topCenter,
Alignment.centerLeft,
Alignment.bottomLeft,
Alignment.bottomRight,
Alignment.bottomCenter,
],
),
),
);
},
codeLink: baseLink('parallax/sandbox_layer.dart'),
info: 'In this example, properties of a layer can be changed to preview '
'the different combination of values',
); );
} }

View File

@ -0,0 +1,69 @@
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
import 'package:flutter/painting.dart';
class SandBoxLayerParallaxGame extends BaseGame {
final Vector2 planeSpeed;
final ImageRepeat planeRepeat;
final LayerFill planeFill;
final Alignment planeAlignment;
SandBoxLayerParallaxGame({
required this.planeSpeed,
required this.planeRepeat,
required this.planeFill,
required this.planeAlignment,
});
@override
Future<void> onLoad() async {
final bgLayer = await loadParallaxLayer(
ParallaxImageData('parallax/bg.png'),
);
final mountainFarLayer = await loadParallaxLayer(
ParallaxImageData('parallax/mountain-far.png'),
velocityMultiplier: Vector2(1.8, 0),
);
final mountainLayer = await loadParallaxLayer(
ParallaxImageData('parallax/mountains.png'),
velocityMultiplier: Vector2(2.8, 0),
);
final treeLayer = await loadParallaxLayer(
ParallaxImageData('parallax/trees.png'),
velocityMultiplier: Vector2(3.8, 0),
);
final foregroundTreesLayer = await loadParallaxLayer(
ParallaxImageData('parallax/foreground-trees.png'),
velocityMultiplier: Vector2(4.8, 0),
);
final airplaneLayer = await loadParallaxLayer(
ParallaxAnimationData(
'parallax/airplane.png',
SpriteAnimationData.sequenced(
amount: 4,
stepTime: 0.2,
textureSize: Vector2(320, 160),
),
),
repeat: planeRepeat,
velocityMultiplier: planeSpeed,
fill: planeFill,
alignment: planeAlignment,
);
final parallax = Parallax(
[
bgLayer,
mountainFarLayer,
mountainLayer,
treeLayer,
foregroundTreesLayer,
airplaneLayer,
],
baseVelocity: Vector2(20, 0),
);
add(ParallaxComponent.fromParallax(parallax));
}
}

View File

@ -1,16 +1,17 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
class SmallParallaxGame extends BaseGame { class SmallParallaxGame extends BaseGame {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
final component = await loadParallaxComponent( final component = await loadParallaxComponent(
[ [
'parallax/bg.png', ParallaxImageData('parallax/bg.png'),
'parallax/mountain-far.png', ParallaxImageData('parallax/mountain-far.png'),
'parallax/mountains.png', ParallaxImageData('parallax/mountains.png'),
'parallax/trees.png', ParallaxImageData('parallax/trees.png'),
'parallax/foreground-trees.png', ParallaxImageData('parallax/foreground-trees.png'),
], ],
size: Vector2.all(200), size: Vector2.all(200),
baseVelocity: Vector2(20, 0), baseVelocity: Vector2(20, 0),

View File

@ -1 +1 @@
42.7 47.7

View File

@ -20,6 +20,9 @@
- Improve error message for composed components - Improve error message for composed components
- Add `anchor` for `ShapeComponent` constructor - Add `anchor` for `ShapeComponent` constructor
- Fix rendering of polygons in `ShapeComponent` - Fix rendering of polygons in `ShapeComponent`
- Add `SpriteAnimation` support to parallax
- Fix `Parallax` alignment for images with different width and height
- Fix `ImageComposition` image bounds validation
## [1.0.0-releasecandidate.11] ## [1.0.0-releasecandidate.11]
- Replace deprecated analysis option lines-of-executable-code with source-lines-of-code - Replace deprecated analysis option lines-of-executable-code with source-lines-of-code

View File

@ -92,8 +92,8 @@ class ImageComposition {
isAntiAlias ??= defaultAntiAlias; isAntiAlias ??= defaultAntiAlias;
assert( assert(
imageRect.contains(source.topLeft) && imageRect.topLeft <= source.topLeft &&
imageRect.contains(source.bottomRight), imageRect.bottomRight >= source.bottomRight,
'Source rect should fit within in the image constraints', 'Source rect should fit within in the image constraints',
); );

View File

@ -12,7 +12,7 @@ import 'position_component.dart';
extension ParallaxComponentExtension on Game { extension ParallaxComponentExtension on Game {
Future<ParallaxComponent> loadParallaxComponent( Future<ParallaxComponent> loadParallaxComponent(
List<String> paths, { List<ParallaxData> dataList, {
Vector2? size, Vector2? size,
Vector2? baseVelocity, Vector2? baseVelocity,
Vector2? velocityMultiplierDelta, Vector2? velocityMultiplierDelta,
@ -22,7 +22,7 @@ extension ParallaxComponentExtension on Game {
int? priority, int? priority,
}) async { }) async {
final component = await ParallaxComponent.load( final component = await ParallaxComponent.load(
paths, dataList,
size: size, size: size,
baseVelocity: baseVelocity, baseVelocity: baseVelocity,
velocityMultiplierDelta: velocityMultiplierDelta, velocityMultiplierDelta: velocityMultiplierDelta,
@ -101,8 +101,9 @@ class ParallaxComponent extends PositionComponent {
/// and filled), otherwise load the [ParallaxLayer]s individually and use the /// and filled), otherwise load the [ParallaxLayer]s individually and use the
/// normal constructor. /// normal constructor.
/// ///
/// [load] takes a list of paths to all the images and a size that you want to use in the /// [load] takes a list of [ParallaxData] of all the images and a size that you want to use in the
/// parallax. /// parallax.
///
/// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta] can be passed /// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta] can be passed
/// in, [baseVelocity] defines what the base velocity of the layers should be /// in, [baseVelocity] defines what the base velocity of the layers should be
/// and [velocityMultiplierDelta] defines how the velocity should change the /// and [velocityMultiplierDelta] defines how the velocity should change the
@ -112,9 +113,10 @@ class ParallaxComponent extends PositionComponent {
/// which edge it should align with ([alignment]), which axis it should fill /// which edge it should align with ([alignment]), which axis it should fill
/// the image on ([fill]) and [images] which is the image cache that should be /// the image on ([fill]) and [images] which is the image cache that should be
/// used can also be passed in. /// used can also be passed in.
///
/// If no image cache is set, the global flame cache is used. /// If no image cache is set, the global flame cache is used.
static Future<ParallaxComponent> load( static Future<ParallaxComponent> load(
List<String> paths, { List<ParallaxData> dataList, {
Vector2? size, Vector2? size,
Vector2? baseVelocity, Vector2? baseVelocity,
Vector2? velocityMultiplierDelta, Vector2? velocityMultiplierDelta,
@ -126,7 +128,7 @@ class ParallaxComponent extends PositionComponent {
}) async { }) async {
final component = ParallaxComponent.fromParallax( final component = ParallaxComponent.fromParallax(
await Parallax.load( await Parallax.load(
paths, dataList,
size: size, size: size,
baseVelocity: baseVelocity, baseVelocity: baseVelocity,
velocityMultiplierDelta: velocityMultiplierDelta, velocityMultiplierDelta: velocityMultiplierDelta,

View File

@ -10,10 +10,11 @@ import 'extensions/rect.dart';
import 'extensions/vector2.dart'; import 'extensions/vector2.dart';
import 'flame.dart'; import 'flame.dart';
import 'game/game.dart'; import 'game/game.dart';
import 'sprite_animation.dart';
extension ParallaxExtension on Game { extension ParallaxExtension on Game {
Future<Parallax> loadParallax( Future<Parallax> loadParallax(
List<String> paths, { List<ParallaxData> dataList, {
Vector2? size, Vector2? size,
Vector2? baseVelocity, Vector2? baseVelocity,
Vector2? velocityMultiplierDelta, Vector2? velocityMultiplierDelta,
@ -22,7 +23,7 @@ extension ParallaxExtension on Game {
LayerFill fill = LayerFill.height, LayerFill fill = LayerFill.height,
}) { }) {
return Parallax.load( return Parallax.load(
paths, dataList,
size: size, size: size,
baseVelocity: baseVelocity, baseVelocity: baseVelocity,
velocityMultiplierDelta: velocityMultiplierDelta, velocityMultiplierDelta: velocityMultiplierDelta,
@ -48,15 +49,32 @@ extension ParallaxExtension on Game {
); );
} }
Future<ParallaxAnimation> loadParallaxAnimation(
String path,
SpriteAnimationData animaitonData, {
ImageRepeat repeat = ImageRepeat.repeatX,
Alignment alignment = Alignment.bottomLeft,
LayerFill fill = LayerFill.height,
}) {
return ParallaxAnimation.load(
path,
animaitonData,
repeat: repeat,
alignment: alignment,
fill: fill,
images: images,
);
}
Future<ParallaxLayer> loadParallaxLayer( Future<ParallaxLayer> loadParallaxLayer(
String path, { ParallaxData data, {
ImageRepeat repeat = ImageRepeat.repeatX, ImageRepeat repeat = ImageRepeat.repeatX,
Alignment alignment = Alignment.bottomLeft, Alignment alignment = Alignment.bottomLeft,
LayerFill fill = LayerFill.height, LayerFill fill = LayerFill.height,
Vector2? velocityMultiplier, Vector2? velocityMultiplier,
}) { }) {
return ParallaxLayer.load( return ParallaxLayer.load(
path, data,
velocityMultiplier: velocityMultiplier, velocityMultiplier: velocityMultiplier,
repeat: repeat, repeat: repeat,
alignment: alignment, alignment: alignment,
@ -66,12 +84,7 @@ extension ParallaxExtension on Game {
} }
} }
/// Specifications with a path to an image and how it should be drawn in abstract class ParallaxRenderer {
/// relation to the device screen
class ParallaxImage {
/// The image
final Image image;
/// If and how the image should be repeated on the canvas /// If and how the image should be repeated on the canvas
final ImageRepeat repeat; final ImageRepeat repeat;
@ -81,12 +94,34 @@ class ParallaxImage {
/// How to fill the screen with the image, always proportionally scaled. /// How to fill the screen with the image, always proportionally scaled.
final LayerFill fill; final LayerFill fill;
ParallaxRenderer({
ImageRepeat? repeat,
Alignment? alignment,
LayerFill? fill,
}) : repeat = repeat ?? ImageRepeat.repeatX,
alignment = alignment ?? Alignment.bottomLeft,
fill = fill ?? LayerFill.height;
void update(double dt);
Image get image;
}
/// Specifications with a path to an image and how it should be drawn in
/// relation to the device screen
class ParallaxImage extends ParallaxRenderer {
/// The image
final Image _image;
ParallaxImage( ParallaxImage(
this.image, { this._image, {
this.repeat = ImageRepeat.repeatX, ImageRepeat? repeat,
this.alignment = Alignment.bottomLeft, Alignment? alignment,
this.fill = LayerFill.height, LayerFill? fill,
}); }) : super(
repeat: repeat,
alignment: alignment,
fill: fill,
);
/// Takes a path of an image, and optionally arguments for how the image should /// Takes a path of an image, and optionally arguments for how the image should
/// repeat ([repeat]), which edge it should align with ([alignment]), which axis /// repeat ([repeat]), which edge it should align with ([alignment]), which axis
@ -107,24 +142,96 @@ class ParallaxImage {
fill: fill, fill: fill,
); );
} }
@override
Image get image => _image;
@override
void update(_) {
// noop
}
}
/// Specifications with a SpriteAnimation and how it should be drawn in
/// relation to the device screen
class ParallaxAnimation extends ParallaxRenderer {
/// The Animation
final SpriteAnimation _animation;
/// The animation's frames prerended into images so it can be used in the parallax
final List<Image> _prerenderedFrames;
ParallaxAnimation(
this._animation,
this._prerenderedFrames, {
ImageRepeat? repeat,
Alignment? alignment,
LayerFill? fill,
}) : super(
repeat: repeat,
alignment: alignment,
fill: fill,
);
/// Takes a path of an image, a SpriteAnimationData, and optionally arguments for how the image should
/// repeat ([repeat]), which edge it should align with ([alignment]), which axis
/// it should fill the image on ([fill]) and [images] which is the image cache
/// that should be used. If no image cache is set, the global flame cache is used.
///
/// _IMPORTANT_: This method pre render all the frames of the animation into image instances
/// so it can be used inside the parallax. Just keep that in mind when using animations in
/// in parallax, the over use of it, or the use of big animations (be it in number of frames
/// or the size of the images) can lead to high use of memory.
static Future<ParallaxAnimation> load(
String path,
SpriteAnimationData animationData, {
ImageRepeat repeat = ImageRepeat.repeatX,
Alignment alignment = Alignment.bottomLeft,
LayerFill fill = LayerFill.height,
Images? images,
}) async {
images ??= Flame.images;
final animation =
await SpriteAnimation.load(path, animationData, images: images);
final prerendedFrames = await Future.wait(
animation.frames.map((frame) => frame.sprite.toImage()).toList(),
);
return ParallaxAnimation(
animation,
prerendedFrames,
repeat: repeat,
alignment: alignment,
fill: fill,
);
}
@override
Image get image => _prerenderedFrames[_animation.currentIndex];
@override
void update(double dt) {
_animation.update(dt);
}
} }
/// Represents one layer in the parallax, draws out an image on a canvas in the /// Represents one layer in the parallax, draws out an image on a canvas in the
/// manner specified by the parallaxImage /// manner specified by the parallaxImage
class ParallaxLayer { class ParallaxLayer {
final ParallaxImage parallaxImage; final ParallaxRenderer parallaxRenderer;
late Vector2 velocityMultiplier; late Vector2 velocityMultiplier;
late Rect _paintArea; late Rect _paintArea;
late Vector2 _scroll; late Vector2 _scroll;
late Vector2 _imageSize; late Vector2 _imageSize;
double _scale = 1.0; double _scale = 1.0;
/// [parallaxImage] is the representation of the image with data of how the /// [parallaxRenderer] is the representation of the renderer with data of how the
/// image should behave. /// layer should behave.
/// [velocityMultiplier] will be used to determine the velocity of the layer by /// [velocityMultiplier] will be used to determine the velocity of the layer by
/// multiplying the [Parallax.baseVelocity] with the [velocityMultiplier]. /// multiplying the [Parallax.baseVelocity] with the [velocityMultiplier].
ParallaxLayer( ParallaxLayer(
this.parallaxImage, { this.parallaxRenderer, {
Vector2? velocityMultiplier, Vector2? velocityMultiplier,
}) : velocityMultiplier = velocityMultiplier ?? Vector2.all(1.0); }) : velocityMultiplier = velocityMultiplier ?? Vector2.all(1.0);
@ -134,18 +241,18 @@ class ParallaxLayer {
double scale(LayerFill fill) { double scale(LayerFill fill) {
switch (fill) { switch (fill) {
case LayerFill.height: case LayerFill.height:
return parallaxImage.image.height / size.y; return parallaxRenderer.image.height / size.y;
case LayerFill.width: case LayerFill.width:
return parallaxImage.image.width / size.x; return parallaxRenderer.image.width / size.x;
default: default:
return _scale; return _scale;
} }
} }
_scale = scale(parallaxImage.fill); _scale = scale(parallaxRenderer.fill);
// The image size so that it fulfills the LayerFill parameter // The image size so that it fulfills the LayerFill parameter
_imageSize = parallaxImage.image.size / _scale; _imageSize = parallaxRenderer.image.size / _scale;
// Number of images that can fit on the canvas plus one // Number of images that can fit on the canvas plus one
// to have something to scroll to without leaving canvas empty // to have something to scroll to without leaving canvas empty
@ -156,9 +263,11 @@ class ParallaxLayer {
..divide(_imageSize); ..divide(_imageSize);
// Align image to correct side of the screen // Align image to correct side of the screen
final alignment = parallaxImage.alignment; final alignment = parallaxRenderer.alignment;
final marginX = alignment.x == 0 ? overflow.x / 2 : alignment.x;
final marginY = alignment.y == 0 ? overflow.y / 2 : alignment.y; final marginX = alignment.x * overflow.x / 2 + overflow.x / 2;
final marginY = alignment.y * overflow.y / 2 + overflow.y / 2;
_scroll = Vector2(marginX, marginY); _scroll = Vector2(marginX, marginY);
// Size of the area to paint the images on // Size of the area to paint the images on
@ -166,10 +275,11 @@ class ParallaxLayer {
_paintArea = paintSize.toRect(); _paintArea = paintSize.toRect();
} }
void update(Vector2 delta) { void update(Vector2 delta, double dt) {
parallaxRenderer.update(dt);
// Scale the delta so that images that are larger don't scroll faster // Scale the delta so that images that are larger don't scroll faster
_scroll += delta.clone()..divide(_imageSize); _scroll += delta.clone()..divide(_imageSize);
switch (parallaxImage.repeat) { switch (parallaxRenderer.repeat) {
case ImageRepeat.repeat: case ImageRepeat.repeat:
_scroll = Vector2(_scroll.x % 1, _scroll.y % 1); _scroll = Vector2(_scroll.x % 1, _scroll.y % 1);
break; break;
@ -198,20 +308,20 @@ class ParallaxLayer {
} }
paintImage( paintImage(
canvas: canvas, canvas: canvas,
image: parallaxImage.image, image: parallaxRenderer.image,
rect: _paintArea, rect: _paintArea,
repeat: parallaxImage.repeat, repeat: parallaxRenderer.repeat,
scale: _scale, scale: _scale,
alignment: parallaxImage.alignment, alignment: parallaxRenderer.alignment,
); );
} }
/// Takes a path of an image, and optionally arguments for how the image should /// Takes a data of a parallax renderer, and optionally arguments for how it should
/// repeat ([repeat]), which edge it should align with ([alignment]), which axis /// repeat ([repeat]), which edge it should align with ([alignment]), which axis
/// it should fill the image on ([fill]) and [images] which is the image cache /// it should fill the image on ([fill]) and [images] which is the image cache
/// that should be used. If no image cache is set, the global flame cache is used. /// that should be used. If no image cache is set, the global flame cache is used.
static Future<ParallaxLayer> load( static Future<ParallaxLayer> load(
String path, { ParallaxData data, {
Vector2? velocityMultiplier, Vector2? velocityMultiplier,
ImageRepeat repeat = ImageRepeat.repeatX, ImageRepeat repeat = ImageRepeat.repeatX,
Alignment alignment = Alignment.bottomLeft, Alignment alignment = Alignment.bottomLeft,
@ -219,12 +329,11 @@ class ParallaxLayer {
Images? images, Images? images,
}) async { }) async {
return ParallaxLayer( return ParallaxLayer(
await ParallaxImage.load( await data.load(
path, repeat,
repeat: repeat, alignment,
alignment: alignment, fill,
fill: fill, images,
images: images,
), ),
velocityMultiplier: velocityMultiplier, velocityMultiplier: velocityMultiplier,
); );
@ -234,6 +343,63 @@ class ParallaxLayer {
/// How to fill the screen with the image, always proportionally scaled. /// How to fill the screen with the image, always proportionally scaled.
enum LayerFill { height, width, none } enum LayerFill { height, width, none }
abstract class ParallaxData {
Future<ParallaxRenderer> load(
ImageRepeat repeat,
Alignment alignment,
LayerFill fill,
Images? images,
);
}
/// Contains the fields and logic to load a [ParallaxImage]
class ParallaxImageData extends ParallaxData {
final String path;
ParallaxImageData(this.path);
@override
Future<ParallaxRenderer> load(
ImageRepeat repeat,
Alignment alignment,
LayerFill fill,
Images? images,
) {
return ParallaxImage.load(
path,
repeat: repeat,
alignment: alignment,
fill: fill,
images: images,
);
}
}
/// Contains the fields and logic to load a [ParallaxAnimation]
class ParallaxAnimationData extends ParallaxData {
final String path;
final SpriteAnimationData animationData;
ParallaxAnimationData(this.path, this.animationData);
@override
Future<ParallaxRenderer> load(
ImageRepeat repeat,
Alignment alignment,
LayerFill fill,
Images? images,
) {
return ParallaxAnimation.load(
path,
animationData,
repeat: repeat,
alignment: alignment,
fill: fill,
images: images,
);
}
}
/// A full parallax, several layers of images drawn out on the screen and each /// A full parallax, several layers of images drawn out on the screen and each
/// layer moves with different velocities to give an effect of depth. /// layer moves with different velocities to give an effect of depth.
class Parallax { class Parallax {
@ -283,6 +449,7 @@ class Parallax {
layers.forEach((layer) { layers.forEach((layer) {
layer.update( layer.update(
(baseVelocity.clone()..multiply(layer.velocityMultiplier)) * dt, (baseVelocity.clone()..multiply(layer.velocityMultiplier)) * dt,
dt,
); );
}); });
} }
@ -305,7 +472,7 @@ class Parallax {
/// used can also be passed in. /// used can also be passed in.
/// If no image cache is set, the global flame cache is used. /// If no image cache is set, the global flame cache is used.
static Future<Parallax> load( static Future<Parallax> load(
List<String> paths, { List<ParallaxData> dataList, {
Vector2? size, Vector2? size,
Vector2? baseVelocity, Vector2? baseVelocity,
Vector2? velocityMultiplierDelta, Vector2? velocityMultiplierDelta,
@ -317,13 +484,12 @@ class Parallax {
final velocityDelta = velocityMultiplierDelta ?? Vector2.all(1.0); final velocityDelta = velocityMultiplierDelta ?? Vector2.all(1.0);
var depth = 0; var depth = 0;
final layers = await Future.wait<ParallaxLayer>( final layers = await Future.wait<ParallaxLayer>(
paths.map((path) async { dataList.map((data) async {
final image = ParallaxImage.load( final renderer = await data.load(
path, repeat,
repeat: repeat, alignment,
alignment: alignment, fill,
fill: fill, images,
images: images,
); );
final velocityMultiplier = final velocityMultiplier =
List.filled(depth, velocityDelta).fold<Vector2>( List.filled(depth, velocityDelta).fold<Vector2>(
@ -332,7 +498,7 @@ class Parallax {
); );
++depth; ++depth;
return ParallaxLayer( return ParallaxLayer(
await image, renderer,
velocityMultiplier: velocityMultiplier, velocityMultiplier: velocityMultiplier,
); );
}), }),

View File

@ -16,6 +16,7 @@ dev_dependencies:
test: ^1.16.0 test: ^1.16.0
dart_code_metrics: ^3.2.2 dart_code_metrics: ^3.2.2
dartdoc: ^0.42.0 dartdoc: ^0.42.0
mocktail: ^0.1.4
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"

View File

@ -0,0 +1,41 @@
import 'dart:ui';
import 'package:flame/image_composition.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
import 'package:flame/extensions.dart';
class MockImage extends Mock implements Image {}
void main() {
group('ImageComposition', () {
group('add', () {
test('breaks assertion when adding an invalid portion', () {
final image = MockImage();
final composition = ImageComposition();
when(() => image.width).thenReturn(100);
when(() => image.height).thenReturn(100);
final invalidRects = [
const Rect.fromLTWH(-10, 10, 10, 10),
const Rect.fromLTWH(10, -10, 10, 10),
const Rect.fromLTWH(110, 10, 10, 10),
const Rect.fromLTWH(0, 110, 10, 10),
const Rect.fromLTWH(0, 0, 110, 110),
const Rect.fromLTWH(20, 0, 90, 10),
const Rect.fromLTWH(0, 20, 90, 90),
const Rect.fromLTWH(0, 0, 190, 90),
const Rect.fromLTWH(0, 0, 90, 190),
];
invalidRects.forEach((r) {
expect(
() => composition.add(image, Vector2.zero(), source: r),
throwsA(isA<AssertionError>()),
);
});
});
});
});
}