Flutter UI Challenge: Animating shapes and their position in Flutter
Recently I was awe-inspired by the portfolio of Karina Sirqueira — from its abstract and minimalistic design to its captivating animations, it’s just mesmerizing.
I wondered if I could replicate this amazing design using Flutter web. After a month of hard work and perseverance, I think I’ve managed to accomplish this task. You can check out the project here.
Although building this amazing project took a lot of coding and debugging, I believe I’ve gained a lot of useful insights from working on this project, and I’d like to share those with you through this series of blog posts. So, if you’d like to come on this journey with me, welcome aboard!
Part 1: Setting up the canvas
Now how would you even start to break this down! There’s so much happening all over the screen!
Let’s start by breaking down the problem and judge what is really happening here:
- The figures change shapes randomly
- The figures change positions randomly
- The animations happen periodically, although it stops when you hover on the shapes
But what looks really random, actually isn’t. If you observe carefully, there are 6 configurations which occur periodically:
Each configuration is unique in some way, but there are some similarities. In the first three configurations and the circular configuration, we can observe that the position for different shapes remain the same, only the shape changes by changing the border radius of the particular shape.
So, how do we implement this in Flutter?
We can see that the shapes overlap each other. The first thing that comes to mind to implement overlapping widgets — Stack, obviously!
Let’s add the shapes first, we’ll worry about animations later. Start with building the models for our shapes.
To place the shapes correctly in the Stack, we will need some properties:
import 'package:flutter/material.dart';
class ShapeConfiguration {
double width, height, top, left;
ShapeBorder shape;
ShapeConfiguration({
required this.width,
required this.height,
required this.top,
required this.left,
required this.shape,
});
}
7 such shapes together form a Figure:
import 'package:portfolio_design/models/shape_configuration.dart';
class FigureConfiguration {
ShapeConfiguration shape1;
ShapeConfiguration shape2;
ShapeConfiguration shape3;
ShapeConfiguration shape4;
ShapeConfiguration shape5;
ShapeConfiguration shape6;
ShapeConfiguration shape7;
FigureConfiguration({
required this.shape1,
required this.shape2,
required this.shape3,
required this.shape4,
required this.shape5,
required this.shape6,
required this.shape7,
});
}
One question you might ask is, why use ShapeBorder? I’ll give you the answer to this shortly.
Let’s initialize our shapes now. But we don’t want our website to be unresponsive, do we?
Therefore, we cannot use hardcoded values for width and height of the shapes. Life would be too easy if that were the case.
Thus we need to use screen size to calculate the shapes’ width and height using MediaQuery.
Unfortunately this is the part where a lot of maths and trial & error come up. What we are doing here essentially is placing the individual shapes separately to our desired position on the screen and giving them a ShapeBorder.
Let’s start with the circular figure first:
final height = MediaQuery.of(context).size.height / 1.5;
final unit = min(height * 1.618, MediaQuery.of(context).size.width) * 0.14;
const double minTopMargin = 10.0;
final double minLeftMargin = isMobileBrowser(context) ? 20.0 : 45.0;
final figure1 = FigureConfiguration(
shape1: ShapeConfiguration(
width: unit * 4,
height: unit * 4,
top: minTopMargin,
left: unit * 3,
shape: const CircleBorder(),
),
shape2: ShapeConfiguration(
width: unit * 2,
height: unit * 2,
top: unit * 2 + minTopMargin,
left: unit * 3 + unit * 2,
shape: const CircleBorder(),
),
shape3: ShapeConfiguration(
width: unit,
height: unit,
top: minTopMargin + unit * 3,
left: unit * 3 * 2,
shape: const CircleBorder(),
),
shape4: ShapeConfiguration(
width: unit * 2,
height: unit * 2,
top: minTopMargin * 2,
left: unit * 1.6,
shape: const CircleBorder(),
),
shape5: ShapeConfiguration(
width: unit * 2,
height: unit * 2,
top: unit * 1.6 + minTopMargin * 2,
left: minLeftMargin,
shape: const CircleBorder(),
),
shape6: ShapeConfiguration(
width: unit,
height: unit,
top: unit * 2 + minTopMargin,
left: unit * 2 + minLeftMargin,
shape: const CircleBorder(),
),
shape7: ShapeConfiguration(
width: unit,
height: unit,
top: unit * 0.8,
left: minLeftMargin + unit * 0.2,
shape: const CircleBorder(),
),
);
🚨MATH ALERT 🚨
The ratio for width:height is 1.618:1 (Guess why!). We multiply the width by 0.14 as there are 7 shapes, so this is essentially dividing the width by 7.
isMobileBrowser will become important later.
Now I know that a lot is going on here, let me try and explain it to you, step-by-step.
Every property of the shape is linked to a common unit variable, so that when the screen size changes, the shapes can recalculate their size and be responsive. Therefore, our unit for measuring the shape’s size, form and position is unit.
This is the part where you can experiment with rearranging the shapes any way you like! Go crazy with your shapes 🤪
Remember, the order of the shapes[shape1,shape2,shape3,…] is IMPORTANT. The order determines which shape goes over the other. Eg. shape7 will be at the front and shape1 will be at the back.
I think that’s enough information in a single post, stay tuned for the second part to know about awesome animations and effects!
Source code can be found here.
Make sure to star the repo 🌟
Peace, love & plants ✌️
Update: Part 2 is released! Check it out here 🚀