mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
Refactor ParallaxComponent (#613)
* Simplified loading of ParallaxComponent * Loading helpers for the different Parallax parts And refactor how the delta velocity works * Fix formatting * Break out Parallax out of ParallaxComponent * Fix docs * Add extension for loading different parallax things on game * Fix formatting * Add loadParallaxComponent extension * Fix formatting
This commit is contained in:
@ -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
|
||||
|
||||
@ -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<void> 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
|
||||
|
||||
|
||||
@ -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<void> onLoad() async {
|
||||
final parallax = await ParallaxComponent.load(
|
||||
_imageNames,
|
||||
baseVelocity: Vector2(20, 0),
|
||||
velocityMultiplierDelta: Vector2(1.8, 1.0),
|
||||
images: images,
|
||||
);
|
||||
|
||||
add(parallaxComponent);
|
||||
add(parallax);
|
||||
}
|
||||
}
|
||||
|
||||
43
doc/examples/parallax/lib/main_advanced.dart
Normal file
43
doc/examples/parallax/lib/main_advanced.dart
Normal file
@ -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<void> 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);
|
||||
}
|
||||
}
|
||||
@ -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<Image> 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<ParallaxComponent> loadParallaxComponent(
|
||||
List<String> 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<Image> _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<ParallaxLayer> _layers;
|
||||
final List<ParallaxImage> _images;
|
||||
final Parallax parallax;
|
||||
|
||||
ParallaxComponent(
|
||||
this._images, {
|
||||
this.baseSpeed,
|
||||
this.layerDelta,
|
||||
}) {
|
||||
baseSpeed ??= Vector2.zero();
|
||||
layerDelta ??= Vector2.zero();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> _load(List<ParallaxImage> 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<ParallaxComponent> load(
|
||||
List<String> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
319
lib/parallax.dart
Normal file
319
lib/parallax.dart
Normal file
@ -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<Parallax> loadParallax(
|
||||
List<String> 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<ParallaxImage> 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<ParallaxLayer> 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<ParallaxImage> 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<ParallaxLayer> 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<ParallaxLayer> 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<Parallax> load(
|
||||
List<String> 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<ParallaxLayer>(
|
||||
paths.map((path) async {
|
||||
final image = ParallaxImage.load(
|
||||
path,
|
||||
repeat: repeat,
|
||||
alignment: alignment,
|
||||
fill: fill,
|
||||
images: images,
|
||||
);
|
||||
final velocityMultiplier =
|
||||
List.filled(depth, velocityMultiplierDelta).fold<Vector2>(
|
||||
velocityMultiplierDelta,
|
||||
(previousValue, delta) => previousValue.clone()..multiply(delta),
|
||||
);
|
||||
++depth;
|
||||
return ParallaxLayer(
|
||||
await image,
|
||||
velocityMultiplier: velocityMultiplier,
|
||||
);
|
||||
}),
|
||||
);
|
||||
return Parallax(
|
||||
layers,
|
||||
baseVelocity: baseVelocity,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user