mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 11:43:19 +08:00
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:
@ -276,8 +276,9 @@ For a working example, check the example in the
|
||||
|
||||
## ParallaxComponent
|
||||
|
||||
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.
|
||||
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 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
|
||||
than distant ones.
|
||||
@ -289,7 +290,10 @@ The simplest `ParallaxComponent` is created like this:
|
||||
```dart
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final parallaxComponent = await loadParallaxComponent(['bg.png', 'trees.png']);
|
||||
final parallaxComponent = await loadParallaxComponent([
|
||||
ParallaxImageData('bg.png'),
|
||||
ParallaxImageData('trees.png'),
|
||||
]);
|
||||
add(parallax);
|
||||
}
|
||||
```
|
||||
@ -300,7 +304,10 @@ A ParallaxComponent can also "load itself" by implementing the `onLoad` method:
|
||||
class MyParallaxComponent extends ParallaxComponent with HasGameRef<MyGame> {
|
||||
@override
|
||||
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
|
||||
final parallaxComponent = await loadParallaxComponent(
|
||||
_paths,
|
||||
_dataList,
|
||||
baseVelocity: Vector2(20, 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
|
||||
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`,
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
[examples directory](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/parallax).
|
||||
|
||||
|
||||
BIN
examples/assets/images/parallax/airplane.png
Normal file
BIN
examples/assets/images/parallax/airplane.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
examples/assets/images/parallax/city.png
Normal file
BIN
examples/assets/images/parallax/city.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 378 B |
BIN
examples/assets/images/parallax/heavy_clouded.png
Normal file
BIN
examples/assets/images/parallax/heavy_clouded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
examples/assets/images/parallax/rain.png
Normal file
BIN
examples/assets/images/parallax/rain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -15,7 +15,7 @@ class AdvancedParallaxGame extends BaseGame {
|
||||
Future<void> onLoad() async {
|
||||
final layers = _layersMeta.entries.map(
|
||||
(e) => loadParallaxLayer(
|
||||
e.key,
|
||||
ParallaxImageData(e.key),
|
||||
velocityMultiplier: Vector2(e.value, 1.0),
|
||||
),
|
||||
);
|
||||
|
||||
44
examples/lib/stories/parallax/animation.dart
Normal file
44
examples/lib/stories/parallax/animation.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/parallax.dart';
|
||||
|
||||
class BasicParallaxGame extends BaseGame {
|
||||
final _imageNames = [
|
||||
'parallax/bg.png',
|
||||
'parallax/mountain-far.png',
|
||||
'parallax/mountains.png',
|
||||
'parallax/trees.png',
|
||||
'parallax/foreground-trees.png',
|
||||
ParallaxImageData('parallax/bg.png'),
|
||||
ParallaxImageData('parallax/mountain-far.png'),
|
||||
ParallaxImageData('parallax/mountains.png'),
|
||||
ParallaxImageData('parallax/trees.png'),
|
||||
ParallaxImageData('parallax/foreground-trees.png'),
|
||||
];
|
||||
|
||||
@override
|
||||
|
||||
@ -15,11 +15,11 @@ class MyParallaxComponent extends ParallaxComponent
|
||||
Future<void> onLoad() async {
|
||||
parallax = await gameRef.loadParallax(
|
||||
[
|
||||
'parallax/bg.png',
|
||||
'parallax/mountain-far.png',
|
||||
'parallax/mountains.png',
|
||||
'parallax/trees.png',
|
||||
'parallax/foreground-trees.png',
|
||||
ParallaxImageData('parallax/bg.png'),
|
||||
ParallaxImageData('parallax/mountain-far.png'),
|
||||
ParallaxImageData('parallax/mountains.png'),
|
||||
ParallaxImageData('parallax/trees.png'),
|
||||
ParallaxImageData('parallax/foreground-trees.png'),
|
||||
],
|
||||
baseVelocity: Vector2(20, 0),
|
||||
velocityMultiplierDelta: Vector2(1.8, 1.0),
|
||||
|
||||
@ -16,11 +16,11 @@ class NoFCSParallaxGame extends Game {
|
||||
Future<void> onLoad() async {
|
||||
parallax = await loadParallax(
|
||||
[
|
||||
'parallax/bg.png',
|
||||
'parallax/mountain-far.png',
|
||||
'parallax/mountains.png',
|
||||
'parallax/trees.png',
|
||||
'parallax/foreground-trees.png',
|
||||
ParallaxImageData('parallax/bg.png'),
|
||||
ParallaxImageData('parallax/mountain-far.png'),
|
||||
ParallaxImageData('parallax/mountains.png'),
|
||||
ParallaxImageData('parallax/trees.png'),
|
||||
ParallaxImageData('parallax/foreground-trees.png'),
|
||||
],
|
||||
size: size,
|
||||
baseVelocity: Vector2(20, 0),
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/parallax.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
import '../../commons/commons.dart';
|
||||
import 'advanced.dart';
|
||||
import 'animation.dart';
|
||||
import 'basic.dart';
|
||||
import 'component.dart';
|
||||
import 'no_fcs.dart';
|
||||
import 'sandbox_layer.dart';
|
||||
import 'small_parallax.dart';
|
||||
|
||||
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 '
|
||||
'extended ParallaxComponent',
|
||||
)
|
||||
..add(
|
||||
'Animation',
|
||||
(_) => GameWidget(game: AnimationParallaxGame()),
|
||||
codeLink: baseLink('parallax/animation.dart'),
|
||||
info: 'Shows how to use animations in a parallax',
|
||||
)
|
||||
..add(
|
||||
'Non-fullscreen',
|
||||
(_) => GameWidget(game: SmallParallaxGame()),
|
||||
@ -42,5 +52,45 @@ void addParallaxStories(Dashbook dashbook) {
|
||||
codeLink: baseLink('parallax/advanced.dart'),
|
||||
info: 'Shows how to create a parallax with different velocity deltas on '
|
||||
'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',
|
||||
);
|
||||
}
|
||||
|
||||
69
examples/lib/stories/parallax/sandbox_layer.dart
Normal file
69
examples/lib/stories/parallax/sandbox_layer.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,17 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/parallax.dart';
|
||||
|
||||
class SmallParallaxGame extends BaseGame {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
final component = await loadParallaxComponent(
|
||||
[
|
||||
'parallax/bg.png',
|
||||
'parallax/mountain-far.png',
|
||||
'parallax/mountains.png',
|
||||
'parallax/trees.png',
|
||||
'parallax/foreground-trees.png',
|
||||
ParallaxImageData('parallax/bg.png'),
|
||||
ParallaxImageData('parallax/mountain-far.png'),
|
||||
ParallaxImageData('parallax/mountains.png'),
|
||||
ParallaxImageData('parallax/trees.png'),
|
||||
ParallaxImageData('parallax/foreground-trees.png'),
|
||||
],
|
||||
size: Vector2.all(200),
|
||||
baseVelocity: Vector2(20, 0),
|
||||
|
||||
@ -1 +1 @@
|
||||
42.7
|
||||
47.7
|
||||
|
||||
@ -20,6 +20,9 @@
|
||||
- Improve error message for composed components
|
||||
- Add `anchor` for `ShapeComponent` constructor
|
||||
- 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]
|
||||
- Replace deprecated analysis option lines-of-executable-code with source-lines-of-code
|
||||
|
||||
@ -92,8 +92,8 @@ class ImageComposition {
|
||||
isAntiAlias ??= defaultAntiAlias;
|
||||
|
||||
assert(
|
||||
imageRect.contains(source.topLeft) &&
|
||||
imageRect.contains(source.bottomRight),
|
||||
imageRect.topLeft <= source.topLeft &&
|
||||
imageRect.bottomRight >= source.bottomRight,
|
||||
'Source rect should fit within in the image constraints',
|
||||
);
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import 'position_component.dart';
|
||||
|
||||
extension ParallaxComponentExtension on Game {
|
||||
Future<ParallaxComponent> loadParallaxComponent(
|
||||
List<String> paths, {
|
||||
List<ParallaxData> dataList, {
|
||||
Vector2? size,
|
||||
Vector2? baseVelocity,
|
||||
Vector2? velocityMultiplierDelta,
|
||||
@ -22,7 +22,7 @@ extension ParallaxComponentExtension on Game {
|
||||
int? priority,
|
||||
}) async {
|
||||
final component = await ParallaxComponent.load(
|
||||
paths,
|
||||
dataList,
|
||||
size: size,
|
||||
baseVelocity: baseVelocity,
|
||||
velocityMultiplierDelta: velocityMultiplierDelta,
|
||||
@ -101,8 +101,9 @@ class ParallaxComponent extends PositionComponent {
|
||||
/// and filled), otherwise load the [ParallaxLayer]s individually and use the
|
||||
/// 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.
|
||||
///
|
||||
/// Optionally arguments for the [baseVelocity] and [velocityMultiplierDelta] can be passed
|
||||
/// in, [baseVelocity] defines what the base velocity of the layers should be
|
||||
/// 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
|
||||
/// the image on ([fill]) and [images] which is the image cache that should be
|
||||
/// used can also be passed in.
|
||||
///
|
||||
/// If no image cache is set, the global flame cache is used.
|
||||
static Future<ParallaxComponent> load(
|
||||
List<String> paths, {
|
||||
List<ParallaxData> dataList, {
|
||||
Vector2? size,
|
||||
Vector2? baseVelocity,
|
||||
Vector2? velocityMultiplierDelta,
|
||||
@ -126,7 +128,7 @@ class ParallaxComponent extends PositionComponent {
|
||||
}) async {
|
||||
final component = ParallaxComponent.fromParallax(
|
||||
await Parallax.load(
|
||||
paths,
|
||||
dataList,
|
||||
size: size,
|
||||
baseVelocity: baseVelocity,
|
||||
velocityMultiplierDelta: velocityMultiplierDelta,
|
||||
|
||||
@ -10,10 +10,11 @@ import 'extensions/rect.dart';
|
||||
import 'extensions/vector2.dart';
|
||||
import 'flame.dart';
|
||||
import 'game/game.dart';
|
||||
import 'sprite_animation.dart';
|
||||
|
||||
extension ParallaxExtension on Game {
|
||||
Future<Parallax> loadParallax(
|
||||
List<String> paths, {
|
||||
List<ParallaxData> dataList, {
|
||||
Vector2? size,
|
||||
Vector2? baseVelocity,
|
||||
Vector2? velocityMultiplierDelta,
|
||||
@ -22,7 +23,7 @@ extension ParallaxExtension on Game {
|
||||
LayerFill fill = LayerFill.height,
|
||||
}) {
|
||||
return Parallax.load(
|
||||
paths,
|
||||
dataList,
|
||||
size: size,
|
||||
baseVelocity: baseVelocity,
|
||||
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(
|
||||
String path, {
|
||||
ParallaxData data, {
|
||||
ImageRepeat repeat = ImageRepeat.repeatX,
|
||||
Alignment alignment = Alignment.bottomLeft,
|
||||
LayerFill fill = LayerFill.height,
|
||||
Vector2? velocityMultiplier,
|
||||
}) {
|
||||
return ParallaxLayer.load(
|
||||
path,
|
||||
data,
|
||||
velocityMultiplier: velocityMultiplier,
|
||||
repeat: repeat,
|
||||
alignment: alignment,
|
||||
@ -66,12 +84,7 @@ extension ParallaxExtension on Game {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifications with a path to an image and how it should be drawn in
|
||||
/// relation to the device screen
|
||||
class ParallaxImage {
|
||||
/// The image
|
||||
final Image image;
|
||||
|
||||
abstract class ParallaxRenderer {
|
||||
/// If and how the image should be repeated on the canvas
|
||||
final ImageRepeat repeat;
|
||||
|
||||
@ -81,12 +94,34 @@ class ParallaxImage {
|
||||
/// How to fill the screen with the image, always proportionally scaled.
|
||||
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(
|
||||
this.image, {
|
||||
this.repeat = ImageRepeat.repeatX,
|
||||
this.alignment = Alignment.bottomLeft,
|
||||
this.fill = LayerFill.height,
|
||||
});
|
||||
this._image, {
|
||||
ImageRepeat? repeat,
|
||||
Alignment? alignment,
|
||||
LayerFill? fill,
|
||||
}) : super(
|
||||
repeat: repeat,
|
||||
alignment: alignment,
|
||||
fill: fill,
|
||||
);
|
||||
|
||||
/// 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
|
||||
@ -107,24 +142,96 @@ class ParallaxImage {
|
||||
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
|
||||
/// manner specified by the parallaxImage
|
||||
class ParallaxLayer {
|
||||
final ParallaxImage parallaxImage;
|
||||
final ParallaxRenderer parallaxRenderer;
|
||||
late Vector2 velocityMultiplier;
|
||||
late Rect _paintArea;
|
||||
late Vector2 _scroll;
|
||||
late Vector2 _imageSize;
|
||||
double _scale = 1.0;
|
||||
|
||||
/// [parallaxImage] is the representation of the image with data of how the
|
||||
/// image should behave.
|
||||
/// [parallaxRenderer] is the representation of the renderer with data of how the
|
||||
/// layer should behave.
|
||||
/// [velocityMultiplier] will be used to determine the velocity of the layer by
|
||||
/// multiplying the [Parallax.baseVelocity] with the [velocityMultiplier].
|
||||
ParallaxLayer(
|
||||
this.parallaxImage, {
|
||||
this.parallaxRenderer, {
|
||||
Vector2? velocityMultiplier,
|
||||
}) : velocityMultiplier = velocityMultiplier ?? Vector2.all(1.0);
|
||||
|
||||
@ -134,18 +241,18 @@ class ParallaxLayer {
|
||||
double scale(LayerFill fill) {
|
||||
switch (fill) {
|
||||
case LayerFill.height:
|
||||
return parallaxImage.image.height / size.y;
|
||||
return parallaxRenderer.image.height / size.y;
|
||||
case LayerFill.width:
|
||||
return parallaxImage.image.width / size.x;
|
||||
return parallaxRenderer.image.width / size.x;
|
||||
default:
|
||||
return _scale;
|
||||
}
|
||||
}
|
||||
|
||||
_scale = scale(parallaxImage.fill);
|
||||
_scale = scale(parallaxRenderer.fill);
|
||||
|
||||
// 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
|
||||
// to have something to scroll to without leaving canvas empty
|
||||
@ -156,9 +263,11 @@ class ParallaxLayer {
|
||||
..divide(_imageSize);
|
||||
|
||||
// Align image to correct side of the screen
|
||||
final alignment = parallaxImage.alignment;
|
||||
final marginX = alignment.x == 0 ? overflow.x / 2 : alignment.x;
|
||||
final marginY = alignment.y == 0 ? overflow.y / 2 : alignment.y;
|
||||
final alignment = parallaxRenderer.alignment;
|
||||
|
||||
final marginX = alignment.x * overflow.x / 2 + overflow.x / 2;
|
||||
final marginY = alignment.y * overflow.y / 2 + overflow.y / 2;
|
||||
|
||||
_scroll = Vector2(marginX, marginY);
|
||||
|
||||
// Size of the area to paint the images on
|
||||
@ -166,10 +275,11 @@ class ParallaxLayer {
|
||||
_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
|
||||
_scroll += delta.clone()..divide(_imageSize);
|
||||
switch (parallaxImage.repeat) {
|
||||
switch (parallaxRenderer.repeat) {
|
||||
case ImageRepeat.repeat:
|
||||
_scroll = Vector2(_scroll.x % 1, _scroll.y % 1);
|
||||
break;
|
||||
@ -198,20 +308,20 @@ class ParallaxLayer {
|
||||
}
|
||||
paintImage(
|
||||
canvas: canvas,
|
||||
image: parallaxImage.image,
|
||||
image: parallaxRenderer.image,
|
||||
rect: _paintArea,
|
||||
repeat: parallaxImage.repeat,
|
||||
repeat: parallaxRenderer.repeat,
|
||||
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
|
||||
/// 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.
|
||||
static Future<ParallaxLayer> load(
|
||||
String path, {
|
||||
ParallaxData data, {
|
||||
Vector2? velocityMultiplier,
|
||||
ImageRepeat repeat = ImageRepeat.repeatX,
|
||||
Alignment alignment = Alignment.bottomLeft,
|
||||
@ -219,12 +329,11 @@ class ParallaxLayer {
|
||||
Images? images,
|
||||
}) async {
|
||||
return ParallaxLayer(
|
||||
await ParallaxImage.load(
|
||||
path,
|
||||
repeat: repeat,
|
||||
alignment: alignment,
|
||||
fill: fill,
|
||||
images: images,
|
||||
await data.load(
|
||||
repeat,
|
||||
alignment,
|
||||
fill,
|
||||
images,
|
||||
),
|
||||
velocityMultiplier: velocityMultiplier,
|
||||
);
|
||||
@ -234,6 +343,63 @@ class ParallaxLayer {
|
||||
/// How to fill the screen with the image, always proportionally scaled.
|
||||
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
|
||||
/// layer moves with different velocities to give an effect of depth.
|
||||
class Parallax {
|
||||
@ -283,6 +449,7 @@ class Parallax {
|
||||
layers.forEach((layer) {
|
||||
layer.update(
|
||||
(baseVelocity.clone()..multiply(layer.velocityMultiplier)) * dt,
|
||||
dt,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -305,7 +472,7 @@ class Parallax {
|
||||
/// used can also be passed in.
|
||||
/// If no image cache is set, the global flame cache is used.
|
||||
static Future<Parallax> load(
|
||||
List<String> paths, {
|
||||
List<ParallaxData> dataList, {
|
||||
Vector2? size,
|
||||
Vector2? baseVelocity,
|
||||
Vector2? velocityMultiplierDelta,
|
||||
@ -317,13 +484,12 @@ class Parallax {
|
||||
final velocityDelta = velocityMultiplierDelta ?? Vector2.all(1.0);
|
||||
var depth = 0;
|
||||
final layers = await Future.wait<ParallaxLayer>(
|
||||
paths.map((path) async {
|
||||
final image = ParallaxImage.load(
|
||||
path,
|
||||
repeat: repeat,
|
||||
alignment: alignment,
|
||||
fill: fill,
|
||||
images: images,
|
||||
dataList.map((data) async {
|
||||
final renderer = await data.load(
|
||||
repeat,
|
||||
alignment,
|
||||
fill,
|
||||
images,
|
||||
);
|
||||
final velocityMultiplier =
|
||||
List.filled(depth, velocityDelta).fold<Vector2>(
|
||||
@ -332,7 +498,7 @@ class Parallax {
|
||||
);
|
||||
++depth;
|
||||
return ParallaxLayer(
|
||||
await image,
|
||||
renderer,
|
||||
velocityMultiplier: velocityMultiplier,
|
||||
);
|
||||
}),
|
||||
|
||||
@ -16,6 +16,7 @@ dev_dependencies:
|
||||
test: ^1.16.0
|
||||
dart_code_metrics: ^3.2.2
|
||||
dartdoc: ^0.42.0
|
||||
mocktail: ^0.1.4
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
41
packages/flame/test/image_composition_test.dart
Normal file
41
packages/flame/test/image_composition_test.dart
Normal 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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user