A couple of weeks ago we held a Flare workshop at the Flutter NYC meetup at Google in New York. As part of the workshop, we demonstrated how to build an interactive tilt card animation from scratch using Flare and Flutter. We also announced, with our friends from Very Good Ventures, the Hamilton Flare Design Challenge!

Hamilton Flare Design Challenge

Be sure to read all the details at the Very Good Ventures announcement. Any Hamilton inspired animation is accepted!

The Flare Workshop

The purpose of the workshop was to show how to build animated graphics for an app directly in Flare. We demonstrated how you can quickly go from concept to final assets without stalling development. The workshop focused primarily on the design and animation side in Flare.

This is what we built:

If you’d like to follow along with the workshop and learn how we created this in Flare, then check out the video below! Grab the files you need from our GitHub repository for the project.

Thanks to Flutter NYC and Martin Rybak for editing this!

How the Flutter Tilt Code Works

As we mentioned, the workshop was mostly about Flare. But what about the Flutter part? If you’re looking for a more in-depth explanation of how the tilt code works, then you’ve come to the right place!

This part of the post is written by Luigi Rosso.

The tilt effect we’re showing uses 4x4 matrix transforms to get your 2d content into 3d space. The first thing to figure out is how we want to let Flutter know how to place arbitrary shapes from our Flare files in 3d space. We decided to make it simple for this example: the 3d layers are going to be determined by your Flare hierarchy. Note that you could do this in a lot of different ways (using custom properties, named shapes, etc).

In Flare

Each layer increments the depth effect so the more layers you have the more depth your final animation will have. You can use nodes to group content to keep multiple shapes or images in the same layer.

With that in mind, I set up a really simple example which you can see here.

Hierarchy

There are four top-level items here and they happen to be shapes, so that means that there will be four layers, each with one shape.

Here’s a quick example of how that translates to 3d space (note that I just turned and squashed the shapes in 2d to simulate this here).

Animation

I also wanted a really simple looping animation that shows the content is actually doing something while it’s being tilted in 3d. Again, the second image is just faking it by rotating and squashing, but it helps visualize the layers.

In Flutter

In order to allow the effect to work with any Flare file, we created a custom FlareTiltWidget which takes a Flare file and draws each top-level (first node in the hierarchy under the artboard) as a 3d layer.

Since Flare currently only works in 2d space, we need to customize the various shape and image renderers that Flare uses in Flutter to premultiply our 4x4 transformations before drawing each layer.

Flare Architecture

A Flare file is represented at runtime by an abstract object called an Actor. The Actor can contain multiple Artboards, but for the purposes of this example it will have only one. Each Artboard can contain multiple animations, but again this example has only one which plays in an infinite loop. An Artboard can be instanced so that there can be multiple versions of it displayed in different states. The animations never get instanced as they are stateless, they can be applied to any Artboard or ArtboardInstance with a provided time value.

Each Artboard (and ArtboardInstance) contains a list of items that help draw the Flare file at runtime. The important ones we care about here are the Drawables.

Inherit and Override

In order to apply a custom 4x4 transformation to each draw operation, we need to override the default Shapes and Images. Flare makes this possible by letting the developer inherit from the default FlutterActor which is responsible for creating concrete versions of the Artboards, Shapes, Images, and so on.

// We create a custom Actor in order to have it create custom

// versions of the shape image nodes.

class TiltActor extends FlutterActor {



/* ... */ @override

ActorArtboard makeArtboard() {

return TiltArtboard(this);

}



@override

ActorShape makeShapeNode() {

return TiltActorShape();

} @override

ActorImage makeImageNode() {

return TiltActorImage();

}



/* ... */ }

The Image and Shape specializations are basically identical, before drawing we want both of them to transform the canvas with a 4x4 matrix to get us into 3d. They also inherit from TiltDrawable which is a simple abstraction that gives us the ability to store a 4x4 transform on any object we want to draw with tilt.

// This is the custom actor shape we create which

// will override the default draw to introduce the tilt.

class TiltActorShape extends FlutterActorShape implements TiltDrawable {

@override

Matrix4 tiltTransform; @override

void draw(ui.Canvas canvas) {

if (!doesDraw) {

return;

} canvas.save();

canvas.transform(tiltTransform.storage);

super.draw(canvas);

canvas.restore();

}

}

The most interesting part of the code is in our custom TiltArtboard which calculates the 4x4 tilt transforms for all the Shapes and Images.

class TiltArtboard extends FlutterActorArtboard {

TiltArtboard(FlutterActor actor) : super(actor); // call this to transform all the shapes and images into perspective

// tiltDepth is a multiplier to enhance the visual depth effect

// (the example uses this to make the tilt more evident when you touch

// the screen)

void setTilt(double pitch, double yaw, double tiltDepth) {

Matrix4 transform = Matrix4.identity();

Matrix4 perspective = Matrix4.identity()..setEntry(3, 2, 0.001);

// the depth effect brings the shapes closer to the camera so we

// scale everything down a bit to keep it visible

transform.multiply(Matrix4.diagonal3Values(0.7, 0.7, 1.0));

transform.multiply(perspective);

transform.multiply(Matrix4.rotationY(yaw));

transform.multiply(Matrix4.rotationX(pitch)); var rootChildren = root.children;

for (final drawable in drawableNodes) {

if (drawable is TiltDrawable) {

ActorNode topComponent = drawable;

int index = 0;

// safety check for climbing the hierarchy

while (topComponent != null) {

// check if we found the root

if (topComponent.parent == root) {

// This component is right under the root,

// which we use to determine z depth.

index = rootChildren.length - rootChildren.indexOf(topComponent);

break;

}

topComponent = topComponent.parent;

}

// compute and store the tilt transform on each TiltDrawable

Matrix4 tiltTransform = Matrix4.copy(transform);

tiltTransform.multiply(

Matrix4.translationValues(0, 0, -100.0 - index * 35.0 * tiltDepth));

(drawable as TiltDrawable).tiltTransform = tiltTransform;

}

}

}

}

Bringing it Together

The last part that brings it together calling setTilt from the FlareTiltWidget as the animation advances. The pitch and yaw values are incremented as the user touches and drags across the screen with a GestureDetector.

GestureDetector(

onPanUpdate: (DragUpdateDetails drag) {

// Set state to update our pitch and yaw.

setState(() { // Get the dimensions of the screen

var size = MediaQuery.of(context).size; // Rotate pitch and yaw by movement in x & y axes

// divided by the screen size.

// You can tweak the 1 value to increase/decrease

// sensitivity while keeping it relatively similar

// across different devices.

pitch += drag.delta.dy * (1 / size.height);

yaw -= drag.delta.dx * (1 / size.width); });

},

In Summary

I hope this article helps illustrate how versatile Flare files can be. We designed both Flare and the Flare runtimes to allow you to dynamically control your projects at runtime. This example showed you how to override the rendering of every shape and image in your file with custom FlareActor. It also showed you how to make your custom FlareActor interact with external inputs. The FlareTiltWidget is a simple, yet powerful, example of a custom LeafRenderObject using a FlareRenderBox which loads, instances, and draw our custom FlareActor. Let me know in the comments if you have any questions!