Bezier Animations — UI elements to Flutter Widgets (Part 2)

Kamalesh Basu
TheBrand.app
Published in
8 min readFeb 1, 2020

In Part 1 of this Article, we took a look at how we can export paths/shapes from UX design software into our Flutter App while maintaining the design integrity and ensuring we get an accurate representation of the paths created by the designer. I suggest you give it a quick read, before going ahead with this.

The repository for the tutorial and the design file we are working with is the same as Part 1.

In Part 1, we ended up with this main.dart file. We will continue from here:

8. Making the Vector Path Parametric

In order to obtain the two paths in our design file (Adobe XD), we wrote two methods — one for the first path and one for the second (_getInitialPath and _getFinalPathrespectively). This is not very useful as we have no way of obtaining intermediate steps when going from one path to another. And hence we can’t animate our path this way.

So we need to rewrite our clipping method in such a way that it can generate the two paths and any intermediate paths that we will need to animate. We need to first convert our method into a parametric function. A parametric function is a method whose workings (in our case rendering the path) depends on 1 or more variables — it could be class variables or method variables or both.

We are going to use a point-wise linear extrapolation to write our parametric function. Let’s break that down.

A. Point-Wise: If we look at our functions _getInitialPath and _getFinalPath , we will see that the first Bezier segment (a vertical line) that we draw for both paths are:

For first curve:
path.lineTo(-0.003999999999997783 * _xScaling,341.78499999999997 * _yScaling);
For final curve:
path.lineTo(-0.003999999999997783 * _xScaling,217.841 * _yScaling);

There is one point involved in each of these Bezier instructions with an x -coordinate and any -coordinates.

Point-wise extrapolation means, for each and every Bezier segment, we will look at individual points involved in the segment — the initial point, the final point, and the control points. And then we are going to write a parametric function to define these points — so that the parametric function generates the initial and final coordinates of this point and intermediate coordinates of the point. As a matter of fact, for each point, we will write 4 parametric functions for:

  1. x -coordinate
  2. y -coordinate
  3. _xScaling
  4. _yScaling

Each of these is a number of type double .

The scaling factors ensure that the path stretches and fits for all device sizes. Also, the scaling factors are the same for all the points involved in the path. So we only need to write them once and reuse them for all the points.

In Part 1 of this Article, I assumed that both paths have the same number of points. I recommended adding points along the path, if a path has fewer points than the other. As you can see, we need to insist that both paths have the same number of points otherwise we can’t use point-wise parametric functions.

There are techniques that can deal with such sets of paths without adding extra points but I will leave that out for another discussion. In my opinion, point-wise strategy works very well and is very efficient. Hence, you can almost always stick to it.

B. Linear Extrapolation: We have boiled down our problem to writing parametric functions for the 4 above numeric values. So let’s focus on one of them.

Let’s say hypothetically, the y -coordinate of the point in the initial path is 10 and that in the final path is 20 . Line extrapolation means when we are half-way through our animation, the y -coordinate should have reached the value of 15 . If we are 30% -of the way in our animation, the y -coordinate should have reached the value of 13 and so on.

Let’s formalize this idea. Let’s call the initial value of y -coordinate as y1 and the final value as y2 . Then the difference between these two values, deltaY = y2-y1 . Instead of using percentages, we are going to use decimals to denote our animation progress. 0will mean the initial state and 1will mean the final state.

So if my animation (while going from initial to final state) has progressed by an amountprogress , my y -coordinate should be:

y = y1 + deltaY * progress

This is our parametric function for the y-coordinate of the point. And we will do this to each of the 4 numbers associated with the point ( x , y , _xScaling and _yScaling ). And then we will do it for all the points associated with the Bezier segment and then we will do that for all the Bezier segments. And that will be our new getClip method. This process is sometimes called Bezier Tweening.

A quick note. We are using a Linear Extrapolation, which is most often the case. But if you follow this Wikipedia link, you will see other types of extrapolation and you might want to use those for your project. In general, my advice is to use Linear extrapolation for your parametric function and then implement non-linear behavior through your AnimationController. More on it in a bit.

8.1 This is too daunting!

At this point, you might be thinking this is too scary and tedious. For a lot of languages and frameworks, Bezier Tweening libraries already exist. GreenSock is one such library that handles all of this for you if you are dealing with web animations. Currently, at the time of writing, there is no library that I have found that neatly does this for Dart/Flutter. If you know of any, please let me know in the comments.

So I wrote a simple tool, which generates the Dart/Flutter code for doing exactly that.

Dart/Flutter Code Generator for Simple Bezier Tweening

You can set up each path, just as we did in Part 1 of the article. Here is the video on how to set up each path:

Once you hit the Generate ClipPath Tween button, it will generate the parametric function for your paths:

Generated Dart/Flutter code for our Clipper Class

As you can see, this parametric function is making use of the progress parameter which will represent our animation progress. We will define it as a class variable.

9. Toggling with Parametric Function

Before we get to animating, let’s see if we can reproduce the toggling behavior we achieved by using the two clipping methods at the end of Part 1.

  1. Delete the _getInitialPath and _getFinalPath methods.
  2. Replace the getClip method with the method generated by the Bezier Tweening tool.
  3. Delete the class variable state from BezierClipper and define a doubleclass variable progress. Update our constructor and it should be something like:
final double progress;
BezierClipper(this.progress);
@overide
Path getClip(Size size){
//Code generated by Bezier Tweener tool
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => true;

4. Next, we focus on the _AnimationPageState class. Replace state variable with double progress and update the _toggle() method accordingly:

double progress = 0;_toggle(){
setState((){
progress = progress == 0 ? 1 : 0;
});
}

In the build method of the class replace the state variable with progress . If you reload the App, you will see we manage to get back our toggle function, but this time with the Parametric getClipfunction.

10. Parametric function for the Container

We just need to fix one more thing before we can animate. If you look at our design file, the two paths are of different heights.

Heights of Paths are different.

But in our app, the height of the container doesn’t change. We will fix that. In the initial state, the path occupies about 0.405 of the Artboard height and in the second path it is about 0.337 of the Artboard height. We define a new parametric equation for the Container height.

final heightScaling = 0.405 + (0.337 - 0.405) * progress;
final height = MediaQuery.of(context).size.height * heightScaling;

And now when we toggle, the height of the Container also adjusts itself.

11. Animating the Bezier Paths

  1. Add an AnimationController, an Animation variable and boolean to the _AnimationPageState class. We also remove the progress variable.
bool isFinalState = false;
AnimationController _controller;
Animation<double> _animation;

2. Next, we initiate the variables and also setup the disposemethod:

@override
void
initState()
{
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 1000)
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}

Note that we use easeOut and easeIn as our animation curves. This is obtained from the Adobe XD design file and going to the Prototype tab. If you inspect the Animation, it is of type easeOut and so we used easeIn as the reverseCurve. This is how we add non-linearity in our animations and can keep our Path parametric function linear.

3. Next, we need to wrap our ClipPath with an AnimationBuilder widget:

AnimatedBuilder(
animation: _controller,
builder: (context, anim){

final double progress = _animation.value;
final double heightScaling = 0.405 + (0.337 - 0.405) * progress;
final double height = MediaQuery.of(context).size.height * heightScaling;
return ClipPath(
clipper: BezierClipper(progress),
child: Container(
color : Color.fromRGBO(255, 91, 53, 1),
height: height,
),
);
}
),

4. Remove the padding property from the Toggle button Container. We don’t need it anymore.

And our animation is done!

And here is our final main.dart file:

12. Final Thoughts and Improvements

  1. Animation Cancelling: We still need to do a few improvements before this animation is production-ready. We need to address how we cancel an animation-in-progress when some event happens that requires the animation to be interrupted. For example, if we quickly press the Toggle button twice, how do we want to handle that. And there could be other considerations.
  2. Multi-Path Animations: The ideas can be easily extended to animations where the path might have more than two states. For example, in a carousel, the background of each slide might be a different path. And you can easily create a smooth transition for the background as the user browses the slides.
  3. Animation Chaining: In most cases, animations are chained. One animation happens after when one animation is partially or fully completed. It will be quite easy to do that too using a single AnimationController.
  4. Tools: The two simple tools I created are by no means exhaustive. For example, your source SVG files might have other in-built transformations such as rotation and scaling. I haven’t built the tool to handle those cases. And it will be nice if you could just drag-and-drop your SVG files. I do plan to build that at some point but I just wanted to show how anyone can go ahead and build these tools for their own project. Also, I would like to provide a better code formatter for both the tools.
  5. Library: And hopefully, someone comes up with a library that deals with a lot of these stuff out of the box. If nothing comes up, I might take a stab at it. Adobe XD has an XD to Flutter plugin project page. But not much info is available on the page.

I hope some of the concepts discussed helped you better understand the fundamentals of Bezier curves, point-wise linear extrapolation, parametric functions, and Bezier animations. Thanks for reading!

--

--