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.
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:
- a little bit of ☁️
- some ⚡️
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!
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).
☁️
So, you need particles. Well, you think, let’s start with one.
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
.
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
.
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.
In a second or so after, your brain suggests you adding a CustomPainter
which is capable of drawingDrawable
s.
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
.
And sure enough, you see a white circle overlaying the button in the UI.
“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.
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.
After that, you simply wrap your Circle
with Centered
and it starts looking very similar to the usual Flutter code.
Then, you think a tiny bit more and decide to make use of awesome Flutter built-in: Alignment
. 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
.
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.
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.
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.
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
.
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
.
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
.
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.
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.
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
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
feature.
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.
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
.
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
docs and simply swap its builder to return the ParticlePainter
instead. And then use the builder
on Particles
for obtaining a child
for AnimatedBuilder
.
And then modify OutlineButton.build
slightly to use this new interface for Particles
.
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
from dart:ui
.
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.
You’re quite sure this thing will allow you to move your Particle
factory to the next level.
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.
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.
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.
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.
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.
Nothing stops you from utilizing the new behaviors now.
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.
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.
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.
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.
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.
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.
And you simply use it in the OutlineButton
.
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.
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.
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
.
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.
And next, just implement another Particle
, using this behavior, keeping the whole system in symmetry.
And then, simply start using it in the Burst
.
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
.
After that, your effort goes ballistic and you start introducing additional helpers one right after another.
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.
Then, you add support for custom Color
in FadingCircle
.
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.
And then, just for ease of use in future, you enclose all the desired particles as one and call Puff
☁️☁️.
Dropping it to the OutlineButton
is no effort at all.
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!
There are not many things which make us happy, but seeing your response to the article would be such a thing for me 😃. If you ever (or never) had (or hadn’t) to implement particle systems (or any other systems) from scratch (or with tutorial) in the past (or if you own a time machine and going to do that in future, but now) please feel free to write a few lines about that to av@av.codes, or simply down below.
And keep your 🧠 happy, so it synthesises the best drugs for you!