Particle Systems (for Puffs and Zaps) with Flutter

Particle Systems (for Puffs and Zaps) with Flutter

Particle Systems (for Puffs and Zaps) with Flutter

“He doesn’t look like I thought he would look”, you thought. Shaking his firm hand you noticed small reddish dots from chemical burns all over his arm. You were distracted by his discouragingly white shirt and haven’t noticed a small wooden box was shovelled under the table by his left leg. Well, you wouldn’t expect a practising magician to turn to a business meeting in a tailcoat, top hat and with a rabbit in one his white gloves. Reality is always a middle ground of sorts. And you can only imagine what was his image of you as a software engineer.

You two sat together on a shamelessly soft sofa and outlined the future project in high-level details. You could express it with one word — e-commerce. Particle Systems (for Puffs and Zaps) with Flutter

But there were some very interesting specifics to this one. In his own words, he wanted to have a little bit of “puff” ☁️ to the most important actions on the application pages. So that it would have a little bit of “prestige” to it. He explained that it wasn’t strictly necessary for his audience, but would add bonus points compared with the competition. You couldn’t agree more and haven’t pay any conscious attention to quiet noise from under the table.

He also said that it would be good to have some “zap” ⚡️ during checkout workflows to make the user feel more magical already, even before their supplies had shipped. What a wonderful idea, you though whilst a tiny fraction of your consciousness recorded quiet chirping sounds from somewhere below.

So your working plan for next phase looks like:

  1. a little bit of ☁️
  2. some ⚡️

Particle Systems (for Puffs and Zaps) with Flutter

Particle Systems (for Puffs and Zaps) with Flutter

And it becomes apparent to you that you want particles. From your past experience, particles are a silver bullet when it comes to playful interaction. Magic is playful.

You start as usual: flutter create magic , then cd magic and code .. Then you Ctrl+P a pubspec.yaml and hit Ctrl+S so that IDE could initialise all the dependencies. When flutter pub get is finished in Debug console, you simply hit F5, choose the Flutter & Dart option, then hit Enter and select one of the emulators you’re using for development.

Simple and sweet, you think, without even realising that you just hit 51 keys on your keyboard in a row to scaffold the app. What a wonderful device is within your skull! Particle Systems (for Puffs and Zaps) with Flutter

You go to dribbble to check what not to do with an application interface to make it obsolete in two months after a new design trend emerges and after a couple of delightful hours end up with the basic application prototype (initial sources for tutorial under the link). Particle Systems (for Puffs and Zaps) with Flutter

So, you need particles. Well, you think, let’s start with one.

class Particle {
  double x;
  double y;

  Particle({ this.x, this.y });
}

But then, you realise that as per your previous experience it’s better to base abstract classes on their supposed behaviour rather properties. So, you think a bit more and remove the x and y from Particle. And making it abstract by itself. As the only thing which you could think of would be common in this system is that you’ll draw the particles on a Canvas .

abstract class Particle {
  void draw(Canvas canvas);
}

But then it comes to you that you may want to draw other things on your canvas too and it might be useful to mark this behaviour for all of them in a consistent fashion, so you end up with renaming this implementation from Particle to Drawable.

mixin Drawable {
  void draw(Canvas canvas);
}

You look to a ⌚️ and see that you already spent almost 5 minutes yet achieved nothing significant with particles. So you decide to go ballistic and actually draw something when the user taps any of the OutlineButton in the app.

Ok, you think, let it be a simple circle, for now.

mixin Drawable {
  void draw(Canvas canvas);
}

class Circle with Drawable {
  @override
  void draw(Canvas canvas) {
    canvas.drawCircle(Offset.zero, 20, Paint()..color = Colors.white);
  }
}

In a second or so after, your brain suggests you adding a CustomPainter which is capable of drawingDrawables.

class DrawablePainter extends CustomPainter {
  final Drawable child;

  DrawablePainter({this.child});

  @override
  void paint(Canvas canvas, Size size) {
    child.draw(canvas);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Then you realize the brain didn’t suggest that out of the blue and update OutlineButton to draw this Circle , by adding a CustomPaint as a root widget in its build with new and shiny DrawablePainter as painter.

class OutlineButton extends StatelessWidget {
  final Widget child;
  final void Function() onPressed;

  const OutlineButton({
    @required this.child,
    @required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    final borderRadius = BorderRadius.circular(32.0);

    return CustomPaint(
      painter: DrawablePainter(child: Circle()),
      child: ClipRRect(
        borderRadius: borderRadius,
        child: RawMaterialButton(
          onPressed: onPressed,
          child: Container(
              child: child,
              decoration: BoxDecoration(
                borderRadius: borderRadius,
                border: Border.all(
                  color: Colors.white,
                  width: 3.0,
                ),
              )),
        ),
      ),
    );
  }
}

And sure enough, you see a white circle overlaying the button in the UI. Particle Systems (for Puffs and Zaps) with Flutter

“That is something already,” zips through your head. But something deep within your stomach tells you you’re far from finished. After a brief googling session, you find that they call it a “Gut feeling”, not that you haven’t felt them earlier.

“I want this to be centered” is your next thought. You see that there’s Size available for CustomPainter#draw. So it’s time to extend Drawable a bit to match that signature.

mixin Drawable {
  // Adding support for [Size]
  void draw(Canvas canvas, Size size);
}

class Circle with Drawable {
  // Updating signature to expect [Size] to be available
  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawCircle(Offset.zero, 20, Paint()..color = Colors.white);
  }
}

class DrawablePainter extends CustomPainter {
  final Drawable child;

  DrawablePainter({this.child});

  @override
  void paint(Canvas canvas, Size size) {
    // Passing [Size] down the chain
    child.draw(canvas, size);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

Particle Systems (for Puffs and Zaps) with Flutter

After that, it’s time for 🔗 chains! Whenever you are building complex systems with multi-layer behavior you always try to express each possible layer separately, so it’s possible to composite them later in whatever order. To apply that for alignment, you create a new Drawable, called Centered, which could contain other Drawable as its child. You implement alignment itself by using Canvas layering with canvas.save() and canvas.restore() , which as you know from experience is an extremely powerful technique which could bring your drawing as code to a whole new level.

class Centered with Drawable {
  final Drawable child;

  Centered({this.child});

  @override
  void draw(Canvas canvas, Size size) {
    canvas.save();
    canvas.translate(size.width / 2, size.height / 2);
    child.draw(canvas, size);
    canvas.restore();
  }
}

After that, you simply wrap your Circle with Centered and it starts looking very similar to the usual Flutter code.

// in OutlineButton.build
// ...
painter: DrawablePainter(
  child: Centered(
    child: Circle()
  ),
),
// ...

Particle Systems (for Puffs and Zaps) with Flutter

Then, you think a tiny bit more and decide to make use of awesome Flutter built-in: [Alignment](https://api.flutter.dev/flutter/painting/Alignment-class.html). As it would allow you to use the same nice and readable syntax as you get used to in Stack, and make use of the system even closer to “native” Fluter look. So, new Drawable, would take an Alignment object as part of the config and would render it’s Drawable child accordingly, very similar to how it was done by Centered.

class Aligned with Drawable {
  final Alignment alignment;
  final Drawable child;

  Aligned({
    this.alignment = Alignment.center,
    this.child,
  });

  @override
  void draw(Canvas canvas, Size size) {
    var offset = alignment.alongSize(size);

    canvas.save();
    canvas.translate(offset.dx, offset.dy);
    child.draw(canvas, size);
    canvas.restore();
  }
}

And you also swap Centered to Aligned in OutlinteButton and try different alignment options: Alignment.topCenter , Alignment.bottomRight , etc. to see how it changes the behaviour.

// in OutlineButton.build
// ...
child: Aligned(
  alignment: Alignment.center
  child: Circle(),
),
// ...

Particle Systems (for Puffs and Zaps) with Flutter You feel that this system is ready for the next step: animations.

Previously, you have used AnimationController to animate things with Flutter and don’t want this case to be an exception. So what you want to do is somehow start redrawing the CustomPainter when AnimationController frame is ready.

You put your 🤔 face on, but only for as long as it took you to think “There should be a widget for that”.

Sure enough, just less than a minute later, here it is, right in a new tab of your browser: AnimatedBuilder. The core essence of that one is that it takes an AnimationController (just what you want, nice), and rebuilds as soon as it’s being animated.

Great power comes out of greater simplicity creating immense extendability, as it often happens in software.

Your next move is to add a new sibling to Drawable. Without any overthinking (just around 5 minutes spent on naming), you call it Updatable. You just want it to take your AnimationController and… do its magic, whatever.

mixin Updatable {
  void update(Animation animation);
}

You’re opting into implementing fading as first animated behavior, cause it’s simple, and opacity could behave exactly like AnimationController value , just change from 0 to 1.

So, Fading would behave as following, when update is called, it would simply set its opacity to the value of AnimationController. Simple and sweet, you definitely kissed it.

mixin Fading on Updatable {
  double opacity = 0.0;

  @override
  void update(Animation controller) {
    opacity = controller.value;
  }
}

And now, you’re linking it with Drawable behaviour, by creating a new class to utilize this mixin: FadingRect. You want that one to draw a Rect with opacity from Fading.

class FadingRect with Drawable, Updatable, Fading {
  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawRect(
      Rect.fromCenter(center: Offset.zero, width: 10, height: 10),
      Paint()..color = Colors.white.withOpacity(opacity),
    );
  }
}

Just as you hit Enter one last line to finish typing this code, your sight stops for a fraction of second on a tab where you searched for “Gut feeling”. And you think that this FadingRect has a lot of guts to hide from outlying users of it. Your next thought is simple and clear, “Hide the guts”. You decide that’s it’s a good time to actually name all these components combined after what they are. So you create your first blueprint of a Particle.

abstract class Particle implements Drawable, Updatable {
  @override
  void draw(Canvas canvas, Size size) {
    // Does nothing by default
  }

  @override
  void update(Animation controller) {
    // Does nothing by default
  }
}

Despite the class being abstract, you leave implementations of Drawable and Updatable explicitly empty. You find it useful that classes extending Particle won’t have to explicitly implement them all the time.

With that, you rewrite the FadingRect to be based on a Particle.

class FadingRect extends Particle with Fading {
  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawRect(
      Rect.fromCenter(center: Offset.zero, width: 10, height: 10),
      Paint()..color = Colors.white.withOpacity(opacity),
    );
  }
}

You can’t stop yourself from reading the declaration. Fading rect extends particle with fading. It looks like a definition on its own and you like when it’s possible to write code that expressive.

That code almost distracted you from the fact that DrawablePainter doesn’t know what Particle is and can’t work with AnimationController. “World needs a new hero,” booms in your head.

class ParticlePainter extends CustomPainter {
  final Animation controller;
  final Particle particle;

  ParticlePainter({@required this.controller, @required this.particle}) {
    particle.update(controller);
  }

  @override
  void paint(Canvas canvas, Size size) {
    particle.draw(canvas, size);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

World meets new hero without enthusiasm you’d expect. In fact, you’re the only one to witness its birth, and will probably remain an only person knowing of its existence. What a lonely hero you’ve just created.

To make its life a bit less miserable, you think it could use a friend. A Flutter-friend. A parent widget, to be more precise. You don’t want to mess with creating new SingleTickerProviderStateMixin or limit yourself to use only StatefulWidget for your particles, so you decide to create a wrapper which would make creating Puffs and Zaps a bit easier.

So, as you usually do, first of all, you write the code showing how would you like to use it. Or, as smart guys call, API-driven design.

class SimpleParticles extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Particles(
        child: Container(
          color: Colors.red,
        ),
        particle: Aligned(
          child: FadingRect(),
        ),
      ),
    );
  }
}

So, you just want to have a widget which would take another widget as a child, and a particle which would be drawn somehow. That’s a good start, but then you realise that calling code would most likely want to control when particles are appearing.

That is same as calling child widget methods from a parent widget. In Flutter it could be solved by either using [Key](/flutter/keys-what-are-they-good-for-13cb51742e7d) to obtain a reference to children state, or by using builder property pattern. You choose the latter as it assumes fewer restrictions on the client code part. So, instead of passing child to Particles, you pass builder.

For client code to know what to expect, you define a signature for your builder using the awesome [typedef](https://dart.dev/guides/language/language-tour#typedefs) feature.

typedef ParticlesWidgetBuilder = Widget Function(
  BuildContext context,
  AnimationController controller,
);

It would allow client code to make awful things to an AnimationController, to make it spill value in all directions and speeds.

With that in mind, the simplest example evolves slightly more complex simplest example.

class SimpleParticles extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Particles(
        builder: (context, controller) {
          return Container(
            color: Colors.red,
          );
        },
        particle: Aligned(
          child: FadingRect(),
        ),
      ),
    );
  }
}

So, now widget from within the builder could call controller.forward() whenever it’s a good time to start animating the Particles.

You want to illustrate that behavior a bit more details to yourself, so rewrite it with actually using the controller.

class SimpleParticles extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Particles(
        builder: (context, controller) {
          return RawMaterialButton(
            onPressed: () {
              controller.forward();
            },
            child: Text('Fade the rect!'),
          );
        },
        particle: Aligned(
          child: FadingRect(),
        ),
      ),
    );
  }
}

Having such an example, now it’s the easiest part left, to actually implement the Particles to behave in a way you just described.

You start with a scaffold of the basic scaffold from [AnimatedBuilder](https://api.flutter.dev/flutter/widgets/AnimatedBuilder-class.html) docs and simply swap its builder to return the ParticlePainter instead. And then use the builder on Particles for obtaining a child for AnimatedBuilder.

class Particles extends StatefulWidget {
  final Duration duration;
  final Particle particle;
  final ParticlesWidgetBuilder builder;

  const Particles({this.particle, this.duration, this.builder});

  @override
  _ParticlesState createState() => _ParticlesState();
}

class _ParticlesState extends State<Particles>
    with SingleTickerProviderStateMixin {
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(vsync: this, duration: widget.duration);
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, child) {
        return CustomPaint(
          child: child,
          painter: ParticlePainter(
            controller: controller,
            particle: widget.particle,
          ),
        );
      },
      child: widget.builder(context, controller),
    );
  }
}

And then modify OutlineButton.build slightly to use this new interface for Particles.

// in OutlineButton.build
// ...
return Particles(
  duration: const Duration(seconds: 1),
  builder: (context, controller) {
    return ClipRRect(
      borderRadius: borderRadius,
      child: RawMaterialButton(
        onPressed: () {
          controller.forward();
          onPressed();
        },
        child: Container(
          child: child,
          decoration: BoxDecoration(
            borderRadius: borderRadius,
            border: Border.all(
              color: Colors.white,
              width: 3.0,
            ),
          ),
        ),
      ),
    );
  },
  particle: Aligned(
    child: FadingRect()
  ),
);
// ...

Particle Systems (for Puffs and Zaps) with Flutter

After adding it to OutlineButton instead of DrawablePainter, it looks pretty boring, you have to admin. But it’s in your full power to make it cooler. The next thing you want to achieve is to make whole thing a bit more explosive 💥, by scaling the rectangle out, as well as to make it fade out instead of fading in.

First things first, you add a new mixin, for scaling, called Scaling using handy [lerpDouble](https://api.flutter.dev/flutter/dart-ui/lerpDouble.html) from dart:ui.

mixin Scaling on Updatable {
  double from = 0; 
  double to = 1;
  double current;

  @override
  void update(Animation controller) {
    current = lerpDouble(from, to, controller.value);
  }
}

Next step is to have a Particle container for this behavior, which would apply the scaling to Canvas and pass control down the chain. Since it’s now quite clear that Particles are going to have a lot of… erm… children, you decide to express that behavior explicitly as well.

mixin NestedParticle on Particle {
  Particle child;

  @override
  void draw(Canvas canvas, Size size) {
    super.draw(canvas, size);
    child.draw(canvas, size);
  }

  @override
  void update(Animation controller) {
    super.update(controller);
    child.update(controller);
  }
}

You’re quite sure this thing will allow you to move your Particle factory to the next level. Particle Systems (for Puffs and Zaps) with Flutter

One neat trick you’re going to use is that in Dart when composing an entity from multiple mixins, it’s still possible to utilise inherited behaviour via using super. The key to that is to be aware that mixins are sequential. So, if someone would ask you to illustrate it, you would show something like the following code.

mixin Loggable {
  log() {
    print('I am loggable!');
  }
}

mixin Loud on Loggable {
  log() {
    print('I am loud');
    super.log();
  }
}

mixin Clear on Loggable {
  log() {
    print('I am clear');
    super.log();
  }
}

// The order of declaration defines what would be "super"
// in each of the mixins above. Try swapping [Loud] and [Clear],
// or move loggable to be declared after [Loggable]
class LoudAndClear with Loggable, Loud, Clear {
  @override
  log() {
    super.log();
    print('Am i loud and clear?');
  }
}

main() {
  var loudAndClear = new LoudAndClear();
  loudAndClear.log();
}

So, with your current setup, if you have more than one mixin competing for base update and draw methods to implement their behavior, it’s just important to not forget to call super, just like you did in NestedParticle.

With that, implementing a ScalingParticle is a breeze.

class ScalingParticle extends Particle with Scaling, NestedParticle {
  double from;
  double to;
  Particle child;

  ScalingParticle({
    this.from = 0.0, 
    this.to = 1.0,
    @required this.child,
  });

  @override
  void draw(Canvas canvas, Size size) {
    canvas.save();
    canvas.scale(current);
    super.draw(canvas, size);
    canvas.restore();
  }
}

You like to think of this pattern as of cascade of control. The rule is just right-to-left, starting with whatever is written in original class method. Particle Systems (for Puffs and Zaps) with Flutter So, during draw phase, ScalingParticle will rescale a new canvas layer and then pass control to NestedParticle which will ensure that child draw is called.

During update phase, as ScalingParticle doesn’t implement any behavior for it, Scaling update will be called, which will set current to an expected scale as per AnimationController value.

You feel great that you don’t have to learn all of this from complete scratch, but if you would, you would refer to this beautiful article. Particle Systems (for Puffs and Zaps) with Flutter

Next part is fading out instead of fading in. Luckily, it’s really easy in your layered system. So you just go to Fading and add a new enum, FadingDirection to represent possible variations of fading behavior, as well as change how update behaves, depending on the value of that enum.

enum FadingDirection { fadeIn, fadeOut }

mixin Fading on Updatable {
  FadingDirection direction = FadingDirection.fadeOut;
  double opacity;

  @override
  void update(Animation controller) {
    if (direction == FadingDirection.fadeOut) {
      opacity = (1 - controller.value);
    } else {
      opacity = controller.value;
    }
  }
}

Nothing stops you from utilizing the new behaviors now. Particle Systems (for Puffs and Zaps) with Flutter

But that looks a bit… meh, so you change the Particles duration to be const Duration(milliseconds: 300). And parameterize the dimensions of FadingRect by adding a Size size field on that particle.

class FadingRect extends Particle with Fading {
  Size size;

  FadingRect({
    this.size = const Size(50, 50),
  });

  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawRect(
      Rect.fromCenter(center: Offset.zero, width: this.size.width, height: this.size.height),
      Paint()..color = Colors.white.withOpacity(opacity),
    );
  }
}

Now, it’s possible to set size of FadingRect to be something like FadingRect(size: Size(200, 200)). And the picture becomes a little bit more enjoyable, resembling a splash after you’re tapping the buttons. Particle Systems (for Puffs and Zaps) with Flutter

A little bit less boring again. You feel that quite soon you’ll move to actually interesting stuff. To make yourself one step closer to “Puff”, you think that you need some way to implement a “burst” of Particle instances in all directions.

You decide that it’s time for a CompositeParticle.

The composition is a good neighbour of layers when it comes to building such systems as per your experience. It happens very often that if you need to operate on a single entity of certain type, you may want to operate over multiple entities too. Another important thing is the symmetry.

Particle Systems (for Puffs and Zaps) with Flutter

Symmetric things are more beautiful to your brain cause brain is never-stopping optimization machine trying to cut as many corners when it comes to computation as possible. No wonder it tries to do that, being responsible for a whopping 20% of your resting metabolic rate, more than any other organ in your body. Symmetry means a simpler way to understand things for your brain, which means less energy spent which makes your brain happy.

Particle Systems (for Puffs and Zaps) with Flutter

You don’t mind your brain being happy at all, so always try to look for ways to apply symmetry to your codebases, so that entities on different levels of your architecture would look and behave symmetrically to each other.

And today is no exception to that symmetry of yours. So, CompositeParticles is symmetrical to how you implemented the NestedParticle behaviour with addition to allowing to have as many (or little) children Particle as needed.

mixin CompositeParticle on Particle {
  List<Particle> children;

  @override
  void draw(Canvas canvas, Size size) {
    super.draw(canvas, size);

    for (var child in children) {
      child.draw(canvas, size);
    }
  }

  @override
  void update(Animation controller) {
    super.update(controller);

    for (var child in children) {
      child.update(controller);
    }
  }
}

With that, it’s quite easy to implement a Burst , a composite particle which just throws its nested children away in random directions while fading them, using all you already prepared.

/// Helpers for randomized dimensions
class Randoms {
  static final rnd = Random();

  /// Returns a random [Offset] from the "center" point of given size
  static Offset offsetFromSize(Size size) {
    return Offset(
      (rnd.nextDouble() * size.width) - (size.width / 2),
      (rnd.nextDouble() * size.height) - (size.height / 2),
    );
  }
}

/// [Burst] takes a list of children [Particle],
/// and wraps each one of them with [MovingParticle] in a random direction
/// from the center of the canvas, within specified [Size]
class Burst extends Particle with CompositeParticle {
  List<Particle> children;

  Burst({
    @required List<Particle> children,
    Size size = const Size(100, 100),
  }) {
    this.children = children
        .map<Particle>(
          (particle) => MovingParticle(
            from: Offset.zero,
            to: Randoms.offsetFromSize(size),
            child: particle,
          ),
        )
        .toList();
  }
}

And you simply use it in the OutlineButton.

// In OutlineButton.build
// ...
particle: Aligned(
  child: Burst(
    children: List<Particle>.generate(
      10,
      (i) => FadingRect(),
    ),
  ),
),
// ...

Particle Systems (for Puffs and Zaps) with Flutter

And the result… Would be considered good if it would be rated by Ghast. So you spend a bit of time adapting it for humans.

You simply swap FadingRect with FadingCircle , which is super easy to implement for you at this stage.

class FadingCircle extends Particle with Fading {
  final double radius;

  FadingCircle({
    this.radius,
  });

  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawCircle(
      Offset.zero,
      radius,
      Paint()..color = Colors.white.withOpacity(opacity),
    );
  }
}

The result looks ok. But you have plans to make it more interesting. First of all, to randomize FadingCircle radiuses, so it looks less repetitive.

// In OutlineButton.build > Particles > Aligned > FadingCircle
// ...
radius: Randoms.rnd.nextDouble() * 10
// ...

And next, is to add easing to the movement of circles in a Burst. Right now it simply linearly interpolates motion between 0 and 1. You this motion to be more organic and life-like.

To do so, in the Particles instead of passing the AnimationController directly to the children ParticlePainter, you decide to pass down the eased version of that Animation, which could be configured, defaulting to Curves.linear.

class Particles extends StatefulWidget {
  final Duration duration;
  final Particle particle;
  final ParticlesWidgetBuilder builder;

  // New property you defined to make animation
  // easing configurable from outside
  final Curve curve;

  const Particles({
    @required this.particle,
    @required this.builder,
    this.duration = const Duration(milliseconds: 400),

    // Default behavior remains as is - no easing
    this.curve = Curves.linear,
  });

  @override
  _ParticlesState createState() => _ParticlesState();
}

class _ParticlesState extends State<Particles>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation animation;


  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: widget.duration,
    );

    // Initialize animation to be passed down to [ParticlePainter] instead
    // of original controller
    animation = CurvedAnimation(curve: widget.curve, parent: controller);
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, child) {
        return CustomPaint(
          child: child,
          painter: ParticlePainter(
            // Passing new curved animation here
            animation: animation,
            particle: widget.particle,
          ),
        );
      },
      child: widget.builder(context, controller),
    );
  }
}

When done, you simply pass Curves.easeOutQuint for that super fluid motion, as well as increase duration to const Duration(seconds: 1) when creating Particles in OutlineButton build. And voila, the prestige. You decide to level up this game up a notch and spread their transition time start a bit. As in many other cases, there’s an awesome Flutter built-in available, just perfect for such a use: Interval.

As Interval is itself a Curve, you decide to go a slightly more generic route and, implement a behavior for using arbitrary Curve instances during Particle update cycle.

mixin Curved on Updatable {
  Curve curve;

  @override
  void update(Animation animation) {
    super.update(
      CurveTween(curve: curve).animate(animation),
    );
  }
}

And next, just implement another Particle, using this behavior, keeping the whole system in symmetry.

class CurvedParticle extends Particle with NestedParticle, Curved {
  Curve curve;
  Particle child;

  CurvedParticle({
    @required this.curve,
    @required this.child,
  });
}

And then, simply start using it in the Burst.

// in Burst constructor, children.map(...)
// ...
(particle) => CurvedParticle(
  curve: Interval(
    // Interval would start at random point from 0 to 0.5
    // and finish at random point between 0.6 and 1.0
    Randoms.rnd.nextDouble() * .5,
    Randoms.rnd.nextDouble() * .4 + .6,
  ),
  child: MovingParticle(
    from: Offset.zero,
    to: Randoms.offsetFromSize(size),
    child: particle,
  ),
),
// ...

Now it’s something you could even show to other people! As another iteration, you add a bit of scaling to these FadingCircle, when creating them inside of OutlineButton.

// In OutlineButton.build > Particles > Aligned
// ...
child: Burst(
  children: List<Particle>.generate(
    20,
    (i) => ScalingParticle(
      child: FadingCircle(radius: Randoms.rnd.nextDouble() * 8 + 2),
      from: .2,
      to: 1.5,
    ),
  ),
),
// ...

After that, your effort goes ballistic and you start introducing additional helpers one right after another.

/// A function which returns [Particle] when called
typedef ParticleProvider = Particle Function(int i);

/// A [CompositeParticle] which allows to use [ParticleProvider] 
/// generator functions as source for children particles
/// 
/// ```dart
/// // 10 plain fading circles
/// ParticleGenerator(10, (i) => FadingCircle());
/// 
/// // 10 randomly sized fading circles
/// ParticleGenerator(10, (i) => FadingCircle(radius: Randoms.rnd.nextDouble() * 10));
/// 
/// // 5 small and 5 large fading circles
/// ParticleGenerator(10, (i) => FadingCircle(radius: i < 5 ? 10 : 20));
/// ```
class ParticleGenerator extends Particle with CompositeParticle {
  List<Particle> children;

  ParticleGenerator(
    int count,
    ParticleProvider generator,
  ) {
    this.children = List<Particle>.generate(count, generator);
  }
}

First of them is ParticleGenerator, allowing you to programmatically generate particles on the fly, a source of even greater variance in what could be achieved.

class FadingCircle extends Particle with Fading {
  final double radius;
  final Color color;

  FadingCircle({
    this.radius = 10,
    this.color = Colors.white,
  });

  @override
  void draw(Canvas canvas, Size size) {
    canvas.drawCircle(
      Offset.zero,
      radius,
      Paint()..color = color.withOpacity(opacity),
    );
  }
}

You also introduce a Circles particle, which draws… (wait for it, just while reading this text in parenthesis…) multiple circles on a Canvas at once, for more puffy shapes.

/// Just a container for parameters 
/// allowing to draw a circle on a [Canvas] later
class CircleParameters {
  double radius;
  Offset offset;

  CircleParameters({@required this.radius, @required this.offset});
}

/// Renders certain amount of circles on a [Canvas],
/// as per given [CircleParameters] list.
class Circles extends Particle with Fading {
  final List<CircleParameters> circles;

  /// Generates randomly positioned circles in
  /// given count with radius between given bounds.
  factory Circles.random({
    int count = 10,
    double maxRadius = 10,
    double minRadius,
  }) {
    if (minRadius == null) {
      minRadius = maxRadius * .1;
    }

    return Circles(
      circles: List<CircleParameters>.generate(
        count,
        (i) => CircleParameters(
          radius: Randoms.rnd.nextDouble() * maxRadius + minRadius,
          offset: Randoms.offsetFromSize(
            Size(maxRadius, maxRadius),
          ),
        ),
      ),
    );
  }

  Circles({this.circles});

  @override
  void draw(Canvas canvas, Size size) {
    for (var circle in circles) {
      canvas.drawCircle(
        circle.offset,
        circle.radius,
        Paint()..color = Colors.white.withOpacity(opacity),
      );
    }
  }
}

And then, just for ease of use in future, you enclose all the desired particles as one and call Puff ☁️☁️.

class Puff extends Particle with NestedParticle {
  Particle child;

  Puff() {
    // Will generate 50 nested particles
    this.child = ParticleGenerator(
      50,
      // Each of which would be positioned randomly
      // within enclosing canvas
      (i) => Aligned(
        alignment: Randoms.alignment(),
        // And with equal possibility would be either
        child: Randoms.rnd.nextDouble() > .5
            // Burst of large white circles flowing
            // slightly upwards
            ? CurvedParticle(
                curve: Interval(
                  .3,
                  1,
                ),
                child: Burst(
                  children: List<Particle>.generate(
                    2,
                    (_) => MovingParticle(
                      child: ScalingParticle(
                        child: Circles.random(count: 2, maxRadius: 50),
                      ),
                      from: Offset.zero,
                      to: Offset(0, -30),
                    ),
                  ),
                ),
              )
            // Or a circle appearing slightly lately
            // and not moving anywhere
            : CurvedParticle(
                curve: Interval(
                  Randoms.rnd.nextDouble() * .5 + .5,
                  1.0,
                  curve: Curves.fastOutSlowIn,
                ),
                child: ScalingParticle(
                  child: FadingCircle(
                    // Of either of two given colors
                    color: Randoms.rnd.nextDouble() > .5
                        ? Palette.accent
                        : Colors.purpleAccent,
                  ),
                ),
              ),
      ),
    );
  }
}

Dropping it to the OutlineButton is no effort at all.

// in OutlineButton.build > Particles
// ...
particle: Puff(),
// ...

And the Puff is live! Despite you have done such systems multiple times in the past, you never get bored with creative freedom it allows you to reach.

You commit the code (full main.dart from the tutorial). You yawn. And you have that feeling of work well done which is one of the best rewards. Best rewards are given to you by your brain which literally gives you drugs to make you feel happier. But that’s another story and you decide that ⚡️ zaps ⚡️ will have to wait till you replenished your mental resource.

Your last thought right before Morpheus takes you is that you’re going to use Interval to make those za…

Thank you for reading this article!

flutter dart mobile-app

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Google's Flutter 1.20 stable announced with new features - Navoki

Google has announced new flutter 1.20 stable with many improvements, and features, enabling flutter for Desktop and Web

Top 25 Flutter Mobile App Templates in 2020

Flutter has been booming worldwide from the past few years. While there are many popular mobile app development technologies out there, Flutter has managed to leave its mark in the mobile application development world. In this article, we’ve curated the best Flutter app templates available on the market as of July 2020.

How To Succeed In Mobile App Wireframe Design?

This article covers everything about mobile app wireframe design: what to do and what not, tools used in designing a mobile or web app wireframe, and more.

What is Flutter and why you should learn it?

Flutter is an open-source UI toolkit for mobile developers, so they can use it to build native-looking Android and iOS applications from the same code base for both platforms. Flutter is also working to make Flutter apps for Web, PWA (progressive Web-App) and Desktop platform (Windows,macOS,Linux).

How much does it cost to make a Flutter app for your business?

Get a Free Quote on Android App Development, iPhone App Development, Ionic App Development, Video Development, ASO, SEO, Google Ads/Adwords, SEO for your app Idea.