LCOV - code coverage report
Current view: top level - lib/src - parallax.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 19 131 14.5 %
Date: 2021-08-10 15:50:53 Functions: 0 0 -

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

Generated by: LCOV version 1.15