diff --git a/CHANGELOG.md b/CHANGELOG.md index 617c94872..8e5759e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add test for child removal - Fix bug where `Timer` callback doesn't fire for non-repeating timers, also fixing bug with `Particle` lifespan - Adding shortcut for loading Sprites and SpriteAnimation from the global cache + - Adding loading methods for the different `ParallaxComponent` parts and refactor how the delta velocity works ## 1.0.0-rc5 - Option for overlays to be already visible on the GameWidget diff --git a/doc/components.md b/doc/components.md index e423ca7e3..75b3b644a 100644 --- a/doc/components.md +++ b/doc/components.md @@ -186,57 +186,85 @@ For a working example, check this [source file](https://github.com/flame-engine/ ## ParallaxComponent -This Component can be used to render pretty backgrounds by drawing several transparent images on top of each other, each dislocated by a tiny amount. +This Component can be used to render pretty backgrounds by drawing several transparent images on top +of each other, where each image 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. +The rationale is that when you look at the horizon and moving, closer objects seem to move faster +than distant ones. This component simulates this effect, making a more realistic background with a feeling of depth. -Create it like this: +The simplest `ParallaxComponent` is created like this: ```dart -final images = [ - ParallaxImage('mountains.jpg'), - ParallaxImage('forest.jpg'), - ParallaxImage('city.jpg'), -]; -this.bg = ParallaxComponent(images); +@override +Future onLoad() async { + final parallaxComponent = await loadParallaxComponent([bg.png, trees.png]); + add(parallax); +} ``` -This creates a static background, if you want it to move you have to set the named optional parameters `baseSpeed` and `layerDelta`. For example if you want to move your background images along the X-axis and have the images further away you would do the following: +This creates a static background, if you want a moving parallax (which is the whole point of a +parallax), you can do it in a few different ways depending on how fine grained you want to set the +settings for each layer. +They simplest way set is to set the named optional parameters `baseVelocity` and +`velocityMultiplierDelta` in the `load` helper function. + +For example if you want to move your background images along the X-axis with a faster speed the +"closer" the image is: ```dart -this.bg = ParallaxComponent(images, baseSpeed: Offset(50, 0), layerDelta: Offset(20, 0)); +final parallaxComponent = await loadParalladComponent( + _paths, + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), +); ``` -You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your game speeds up. +You can set the baseSpeed and layerDelta at any time, for example if your character jumps or your +game speeds up. ```dart -this.bg.baseSpeed = Vector2(100, 0); -this.bg.layerDelta = Vector2(40, 0); +final parallax = parallaxComponen.parallax; +parallax.baseSpeed = Vector2(100, 0); +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 behaviour, for example if you are not making a side scrolling game, you can set the `repeat`, `alignment` and `fill` parameters for each ParallaxImage. +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 +behaviour, 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 +then pass in to the `ParallaxComponent`'s constructor. Advanced example: ```dart final images = [ - ParallaxImage('stars.jpg', repeat: ImageRepeat.repeat, alignment: Alignment.center, fill: LayerFill.width), - ParallaxImage('planets.jpg', repeat: ImageRepeat.repeatY, alignment: Alignment.bottomLeft, fill: LayerFill.none), - ParallaxImage('dust.jpg', repeat: ImageRepeat.repeatX, alignment: Alignment.topRight, fill: LayerFill.height), + loadParallaxImage('stars.jpg', repeat: ImageRepeat.repeat, alignment: Alignment.center, fill: LayerFill.width), + loadParallaxImage('planets.jpg', repeat: ImageRepeat.repeatY, alignment: Alignment.bottomLeft, fill: LayerFill.none), + loadParallaxImage('dust.jpg', repeat: ImageRepeat.repeatX, alignment: Alignment.topRight, fill: LayerFill.height), ]; -this.bg = ParallaxComponent(images, baseSpeed: Vector2(50, 0), layerDelta: Vector2(20, 0)); +final layers = images.map((image) => ParallaxLayer(await image, velocityMulitplier: images.indexOf(image) * 2.0)); +final parallaxComponent = ParallaxComponent( + Parallax( + await Future.wait(layers), + baseVelocity: Vector2(50, 0), + ), +); ``` * The stars image in this example will be repeatedly drawn in both axis, align in the center and be scaled to fill the screen width. * The planets image will be repeated in Y-axis, aligned to the bottom left of the screen and not be scaled. * The dust image will be repeated in X-axis, aligned to the top right and scaled to fill the screen height. -Once you are done with setting the parameters to your needs, render the ParallaxComponent as any other component. - -Like the SpriteAnimationComponent, even if your parallax is static, you must call update on this component, so it runs its animation. +Once you are done setting up your `ParallaxComponent`, add it to the game like with any other +component (`game.add(parallaxComponent`). Also, don't forget to add you images to the `pubspec.yaml` file as assets or they wont be found. -An example implementation can be found in the [examples directory](https://github.com/flame-engine/flame/tree/master/doc/examples/parallax). +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 +one. The same for the `ParallaxComponent` file, but that provides `loadParallaxComponent`. + +Two examples implementation can be found in the +[examples directory](https://github.com/flame-engine/flame/tree/master/doc/examples/parallax). ## SpriteBodyComponent diff --git a/doc/examples/parallax/lib/main.dart b/doc/examples/parallax/lib/main.dart index c09187bc7..045b5c357 100644 --- a/doc/examples/parallax/lib/main.dart +++ b/doc/examples/parallax/lib/main.dart @@ -15,21 +15,22 @@ void main() async { } class MyGame extends BaseGame { - MyGame() { - final images = [ - ParallaxImage('bg.png'), - ParallaxImage('mountain-far.png'), - ParallaxImage('mountains.png'), - ParallaxImage('trees.png'), - ParallaxImage('foreground-trees.png'), - ]; + final _imageNames = [ + 'bg.png', + 'mountain-far.png', + 'mountains.png', + 'trees.png', + 'foreground-trees.png', + ]; - final parallaxComponent = ParallaxComponent( - images, - baseSpeed: Vector2(20, 0), - layerDelta: Vector2(30, 0), + @override + Future onLoad() async { + final parallax = await ParallaxComponent.load( + _imageNames, + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + images: images, ); - - add(parallaxComponent); + add(parallax); } } diff --git a/doc/examples/parallax/lib/main_advanced.dart b/doc/examples/parallax/lib/main_advanced.dart new file mode 100644 index 000000000..691b5e6b0 --- /dev/null +++ b/doc/examples/parallax/lib/main_advanced.dart @@ -0,0 +1,43 @@ +import 'package:flame/flame.dart'; +import 'package:flame/parallax.dart'; +import 'package:flame/components/parallax_component.dart'; +import 'package:flame/extensions/vector2.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Flame.util.fullScreen(); + runApp( + GameWidget( + game: MyGame(), + ), + ); +} + +class MyGame extends BaseGame { + final _layersMeta = { + 'bg.png': 1.0, + 'mountain-far.png': 1.5, + 'mountains.png': 2.3, + 'trees.png': 3.8, + 'foreground-trees.png': 6.6, + }; + + @override + Future onLoad() async { + final layers = _layersMeta.entries.map( + (e) => loadParallaxLayer( + e.key, + velocityMultiplier: Vector2(e.value, 1.0), + ), + ); + final parallax = ParallaxComponent( + Parallax( + await Future.wait(layers), + baseVelocity: Vector2(20, 0), + ), + ); + add(parallax); + } +} diff --git a/lib/components/parallax_component.dart b/lib/components/parallax_component.dart index 3887f12cd..854f976fa 100644 --- a/lib/components/parallax_component.dart +++ b/lib/components/parallax_component.dart @@ -4,197 +4,52 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; -import '../extensions/rect.dart'; +import '../assets/images.dart'; import '../extensions/vector2.dart'; -import '../flame.dart'; +import '../game.dart'; +import '../parallax.dart'; import 'position_component.dart'; -/// Specifications with a path to an image and how it should be drawn in -/// relation to the device screen -class ParallaxImage { - /// The filename of the image - final String filename; - - /// If and how the image should be repeated on the canvas - final ImageRepeat repeat; - - /// How to align the image in relation to the screen - final Alignment alignment; - - /// How to fill the screen with the image, always proportionally scaled. - final LayerFill fill; - - ParallaxImage( - this.filename, { - this.repeat = ImageRepeat.repeatX, - this.alignment = Alignment.bottomLeft, - this.fill = LayerFill.height, - }); -} - -/// 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; - Future future; - - Image _image; - Rect _paintArea; - Vector2 _screenSize; - Vector2 _scroll; - Vector2 _imageSize; - double _scale = 1.0; - - ParallaxLayer(this.parallaxImage) { - future = _load(parallaxImage.filename); - } - - bool loaded() => _image != null; - - Vector2 currentOffset() => _scroll; - - void resize(Vector2 size) { - if (!loaded()) { - _screenSize = size; - return; - } - - double scale(LayerFill fill) { - switch (fill) { - case LayerFill.height: - return _image.height / size.y; - case LayerFill.width: - return _image.width / size.x; - default: - return _scale; - } - } - - _scale = scale(parallaxImage.fill); - - // The image size so that it fulfills the LayerFill parameter - _imageSize = - Vector2Extension.fromInts(_image.width, _image.height) / _scale; - - // Number of images that can fit on the canvas plus one - // to have something to scroll to without leaving canvas empty - final Vector2 count = Vector2.all(1) + (size.clone()..divide(_imageSize)); - - // Percentage of the image size that will overflow - final Vector2 overflow = ((_imageSize.clone()..multiply(count)) - size) - ..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; - _scroll ??= Vector2(marginX, marginY); - - // Size of the area to paint the images on - final Vector2 paintSize = count..multiply(_imageSize); - _paintArea = paintSize.toRect(); - } - - void update(Vector2 delta) { - if (!loaded()) { - return; - } - - // Scale the delta so that images that are larger don't scroll faster - _scroll += delta.clone()..divide(_imageSize); - switch (parallaxImage.repeat) { - case ImageRepeat.repeat: - _scroll = Vector2(_scroll.x % 1, _scroll.y % 1); - break; - case ImageRepeat.repeatX: - _scroll = Vector2(_scroll.x % 1, _scroll.y); - break; - case ImageRepeat.repeatY: - _scroll = Vector2(_scroll.x, _scroll.y % 1); - break; - case ImageRepeat.noRepeat: - break; - } - - final Vector2 scrollPosition = _scroll.clone()..multiply(_imageSize); - _paintArea = Rect.fromLTWH( - -scrollPosition.x, - -scrollPosition.y, - _paintArea.width, - _paintArea.height, +extension ParallaxComponentExtension on Game { + Future loadParallaxComponent( + List paths, { + Vector2 baseVelocity, + Vector2 velocityMultiplierDelta, + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + }) { + return ParallaxComponent.load( + paths, + baseVelocity: baseVelocity, + velocityMultiplierDelta: velocityMultiplierDelta, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, ); } - - void render(Canvas canvas) { - if (!loaded()) { - return; - } - - paintImage( - canvas: canvas, - image: _image, - rect: _paintArea, - repeat: parallaxImage.repeat, - scale: _scale, - alignment: parallaxImage.alignment, - ); - } - - Future _load(String filename) { - return Flame.images.load(filename).then((image) { - _image = image; - if (_screenSize != null) { - resize(_screenSize); - } - return _image; - }); - } } -/// How to fill the screen with the image, always proportionally scaled. -enum LayerFill { height, width, none } - /// A full parallax, several layers of images drawn out on the screen and each -/// layer moves with different speeds to give an effect of depth. +/// layer moves with different velocities to give an effect of depth. class ParallaxComponent extends PositionComponent { - Vector2 baseSpeed; - Vector2 layerDelta; - List _layers; - final List _images; + final Parallax parallax; - ParallaxComponent( - this._images, { - this.baseSpeed, - this.layerDelta, - }) { - baseSpeed ??= Vector2.zero(); - layerDelta ??= Vector2.zero(); - } - - @override - Future onLoad() async { - await _load(_images); - _layers?.forEach((layer) => layer.resize(size)); - } - - /// The base offset of the parallax, can be used in an outer update loop - /// if you want to transition the parallax to a certain position. - Vector2 currentOffset() => _layers[0].currentOffset(); + ParallaxComponent(this.parallax); @mustCallSuper @override void onGameResize(Vector2 size) { - this.size = size; super.onGameResize(size); - _layers?.forEach((layer) => layer.resize(size)); + this.size = size; + parallax.resize(size); } @override void update(double t) { super.update(t); - _layers.forEach((layer) { - layer.update(baseSpeed * t + layerDelta * (_layers.indexOf(layer) * t)); - }); + parallax.update(t); } @mustCallSuper @@ -202,12 +57,50 @@ class ParallaxComponent extends PositionComponent { void render(Canvas canvas) { super.render(canvas); canvas.save(); - _layers.forEach((layer) => layer.render(canvas)); + parallax.layers.forEach((layer) { + canvas.save(); + layer.render(canvas); + canvas.restore(); + }); canvas.restore(); } - Future _load(List images) async { - _layers = images.map((image) => ParallaxLayer(image)).toList(); - await Future.wait(_layers.map((layer) => layer.future)); + /// Note that this method only should be used if all of your layers should + /// have the same layer arguments (how the images should be repeated, aligned + /// and filled), otherwise load the [ParallaxLayer]s individually and use the + /// normal constructor. + /// + /// [load] takes a list of paths to all the images that you want to use in the + /// parallax. + /// Optionally arguments for the [baseVelocity] and [layerDelta] can be passed + /// in, [baseVelocity] defines what the base velocity of the layers should be + /// and [velocityMultiplierDelta] defines how the velocity should change the + /// closer the layer is ([velocityMultiplierDelta ^ n], where n is the + /// layer index). + /// Arguments for how all the images 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 can also be passed in. + /// If no image cache is set, the global flame cache is used. + static Future load( + List paths, { + Vector2 baseVelocity, + Vector2 velocityMultiplierDelta, + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + Images images, + }) async { + return ParallaxComponent( + await Parallax.load( + paths, + baseVelocity: baseVelocity, + velocityMultiplierDelta: velocityMultiplierDelta, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ), + ); } } diff --git a/lib/parallax.dart b/lib/parallax.dart new file mode 100644 index 000000000..8f943034b --- /dev/null +++ b/lib/parallax.dart @@ -0,0 +1,319 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/painting.dart'; + +import 'assets/images.dart'; +import 'extensions/rect.dart'; +import 'extensions/vector2.dart'; +import 'flame.dart'; +import 'game/game.dart'; + +extension ParallaxExtension on Game { + Future loadParallax( + List paths, { + Vector2 baseVelocity, + Vector2 velocityMultiplierDelta, + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + }) { + return Parallax.load( + paths, + baseVelocity: baseVelocity, + velocityMultiplierDelta: velocityMultiplierDelta, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ); + } + + Future loadParallaxImage( + String path, { + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + }) { + return ParallaxImage.load( + path, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ); + } + + Future loadParallaxLayer( + String path, { + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + Vector2 velocityMultiplier, + }) { + return ParallaxLayer.load( + path, + velocityMultiplier: velocityMultiplier, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ); + } +} + +/// 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; + + /// If and how the image should be repeated on the canvas + final ImageRepeat repeat; + + /// How to align the image in relation to the screen + final Alignment alignment; + + /// How to fill the screen with the image, always proportionally scaled. + final LayerFill fill; + + ParallaxImage( + this.image, { + this.repeat = ImageRepeat.repeatX, + this.alignment = Alignment.bottomLeft, + this.fill = LayerFill.height, + }); + + /// 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 + /// 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 load( + String path, { + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + Images images, + }) async { + images ??= Flame.images; + return ParallaxImage( + await images.load(path), + repeat: repeat, + alignment: alignment, + fill: fill, + ); + } +} + +/// 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; + Vector2 velocityMultiplier; + Rect _paintArea; + Vector2 _scroll; + Vector2 _imageSize; + double _scale = 1.0; + + /// [parallaxImage] is the representation of the image with data of how the + /// image should behave. + /// [velocityMultiplier] will be used to determine the velocity of the layer by + /// multiplying the [baseVelocity] with the [velocityMultiplier]. + ParallaxLayer( + this.parallaxImage, { + this.velocityMultiplier, + }) { + velocityMultiplier ??= Vector2.all(1.0); + } + + Vector2 currentOffset() => _scroll; + + void resize(Vector2 size) { + double scale(LayerFill fill) { + switch (fill) { + case LayerFill.height: + return parallaxImage.image.height / size.y; + case LayerFill.width: + return parallaxImage.image.width / size.x; + default: + return _scale; + } + } + + _scale = scale(parallaxImage.fill); + + // The image size so that it fulfills the LayerFill parameter + _imageSize = Vector2Extension.fromInts( + parallaxImage.image.width, + parallaxImage.image.height, + ) / + _scale; + + // Number of images that can fit on the canvas plus one + // to have something to scroll to without leaving canvas empty + final Vector2 count = Vector2.all(1) + (size.clone()..divide(_imageSize)); + + // Percentage of the image size that will overflow + final Vector2 overflow = ((_imageSize.clone()..multiply(count)) - size) + ..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; + _scroll ??= Vector2(marginX, marginY); + + // Size of the area to paint the images on + final Vector2 paintSize = count..multiply(_imageSize); + _paintArea = paintSize.toRect(); + } + + void update(Vector2 delta) { + // Scale the delta so that images that are larger don't scroll faster + _scroll += delta.clone()..divide(_imageSize); + switch (parallaxImage.repeat) { + case ImageRepeat.repeat: + _scroll = Vector2(_scroll.x % 1, _scroll.y % 1); + break; + case ImageRepeat.repeatX: + _scroll = Vector2(_scroll.x % 1, _scroll.y); + break; + case ImageRepeat.repeatY: + _scroll = Vector2(_scroll.x, _scroll.y % 1); + break; + case ImageRepeat.noRepeat: + break; + } + + final Vector2 scrollPosition = _scroll.clone()..multiply(_imageSize); + _paintArea = Rect.fromLTWH( + -scrollPosition.x, + -scrollPosition.y, + _paintArea.width, + _paintArea.height, + ); + } + + void render(Canvas canvas) { + paintImage( + canvas: canvas, + image: parallaxImage.image, + rect: _paintArea, + repeat: parallaxImage.repeat, + scale: _scale, + alignment: parallaxImage.alignment, + ); + } + + /// 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 + /// 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 load( + String path, { + Vector2 velocityMultiplier, + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + Images images, + }) async { + return ParallaxLayer( + await ParallaxImage.load( + path, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ), + velocityMultiplier: velocityMultiplier, + ); + } +} + +/// How to fill the screen with the image, always proportionally scaled. +enum LayerFill { height, width, none } + +/// 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 { + Vector2 baseVelocity; + final List layers; + + Parallax( + this.layers, { + this.baseVelocity, + }) { + baseVelocity ??= Vector2.zero(); + } + + /// The base offset of the parallax, can be used in an outer update loop + /// if you want to transition the parallax to a certain position. + Vector2 currentOffset() => layers[0].currentOffset(); + + /// If the `ParallaxComponent` isn't used your own wrapper needs to call this + /// on creation. + void resize(Vector2 size) => layers.forEach((layer) => layer.resize(size)); + + void update(double t) { + layers.forEach((layer) { + layer.update( + (baseVelocity.clone()..multiply(layer.velocityMultiplier)) * t, + ); + }); + } + + /// Note that this method only should be used if all of your layers should + /// have the same layer arguments (how the images should be repeated, aligned + /// and filled), otherwise load the [ParallaxLayer]s individually and use the + /// normal constructor. + /// + /// [load] takes a list of paths to all the images that you want to use in the + /// parallax. + /// Optionally arguments for the [baseVelocity] and [layerDelta] can be passed + /// in, [baseVelocity] defines what the base velocity of the layers should be + /// and [velocityMultiplierDelta] defines how the velocity should change the + /// closer the layer is ([velocityMultiplierDelta ^ n], where n is the + /// layer index). + /// Arguments for how all the images 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 can also be passed in. + /// If no image cache is set, the global flame cache is used. + static Future load( + List paths, { + Vector2 baseVelocity, + Vector2 velocityMultiplierDelta, + ImageRepeat repeat = ImageRepeat.repeatX, + Alignment alignment = Alignment.bottomLeft, + LayerFill fill = LayerFill.height, + Images images, + }) async { + velocityMultiplierDelta ??= Vector2.all(1.0); + int depth = 0; + final layers = await Future.wait( + paths.map((path) async { + final image = ParallaxImage.load( + path, + repeat: repeat, + alignment: alignment, + fill: fill, + images: images, + ); + final velocityMultiplier = + List.filled(depth, velocityMultiplierDelta).fold( + velocityMultiplierDelta, + (previousValue, delta) => previousValue.clone()..multiply(delta), + ); + ++depth; + return ParallaxLayer( + await image, + velocityMultiplier: velocityMultiplier, + ); + }), + ); + return Parallax( + layers, + baseVelocity: baseVelocity, + ); + } +}