Created
July 11, 2022 09:53
-
-
Save pietervp/ed14c24ed37676d70275dc150552ed8b to your computer and use it in GitHub Desktop.
Example Animation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'dart:math'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/scheduler.dart'; | |
import 'package:flutter/widgets.dart'; | |
const double animationDurationInSeconds = 5.0; | |
void main() => runApp(ExampleFunvasWidget()); | |
/// The animation is drawn in [u] based on [t] in seconds. | |
class ExampleFunvas extends Funvas { | |
static const _n = 2560; | |
@override | |
void u(double t) { | |
final d = s2q(750).width; | |
c.drawColor(const Color(0xff000000), BlendMode.srcOver); | |
c.translate(d / 2, d / 2); | |
drawFrame(t, d, 1); | |
} | |
void drawFrame(double t, double d, double alpha) { | |
for (var i = 0; i < _n; i++) { | |
c.drawCircle( | |
Offset( | |
cos(pi * t + 2 * pi / _n * i), | |
cos(i / _n * pi / 2 + pi * t + 2 * pi / _n * i), | |
) * | |
(d / 3), | |
10, | |
Paint() | |
..color = HSVColor.fromAHSV( | |
alpha, | |
360 / _n * i, | |
3 / 4, | |
3 / 4, | |
).toColor(), | |
); | |
} | |
} | |
} | |
/// Example widget that displays the [ExampleFunvas] animation. | |
class ExampleFunvasWidget extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return SizedBox( | |
width: 420, | |
height: 420, | |
child: FunvasContainer( | |
funvas: ExampleFunvas(), | |
), | |
); | |
} | |
} | |
/// Widget that allows you to insert a [Funvas] into the widget tree. | |
/// | |
/// The size available to the funvas is the biggest constraint available to this | |
/// widget. Therefore, you can size the [FunvasContainer] by wrapping it in a | |
/// [SizedBox] for example. | |
class FunvasContainer extends StatefulWidget { | |
/// Creates a container that the provided [funvas] can draw in. | |
/// | |
/// Size and position are provided by the container, i.e. they are provided by | |
/// the widget tree above the widget. | |
/// | |
/// If the [funvas] is changed for the same element in the element tree, the | |
/// timer on the state will reset, restarting [Funvas.u] at `0` seconds. | |
const FunvasContainer({ | |
Key? key, | |
this.paused = false, | |
required this.funvas, | |
}) : super(key: key); | |
/// Whether the [funvas] animation should be paused or not. | |
/// | |
/// This is implemented using [Ticker.muted], which means that pausing the | |
/// container will stop the animation from being redrawn. However, the time | |
/// in the ticker will still elapse. This means that disabling [paused] again | |
/// will cause the animation to jump to the point it would have been at if it | |
/// had never been paused. | |
/// | |
/// To display a funvas animation that never ticks, i.e. displays only a | |
/// single frame without any side effects, one should instead use a | |
/// [CustomPaint] with a [FunvasPainter] that is passed a static time instead. | |
/// | |
/// Defaults to false. | |
final bool paused; | |
/// The [Funvas] that can draw in the container. | |
final Funvas funvas; | |
@override | |
State<FunvasContainer> createState() => _FunvasContainerState(); | |
} | |
class _FunvasContainerState extends State<FunvasContainer> | |
with SingleTickerProviderStateMixin { | |
late final ValueNotifier<double> _time; | |
late final Ticker _ticker; | |
@override | |
void initState() { | |
super.initState(); | |
_time = ValueNotifier(0); | |
_ticker = createTicker(_update); | |
if (!widget.paused) { | |
_ticker.start(); | |
} | |
} | |
@override | |
void didUpdateWidget(covariant FunvasContainer oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (oldWidget.funvas != widget.funvas) { | |
_time.value = 0; | |
_ticker | |
..stop() | |
..start(); | |
} | |
if (!widget.paused && oldWidget.paused && !_ticker.isActive) { | |
_ticker | |
..muted = false | |
..start(); | |
} else { | |
_ticker.muted = widget.paused; | |
} | |
} | |
@override | |
void dispose() { | |
_time.dispose(); | |
_ticker.dispose(); | |
super.dispose(); | |
} | |
void _update(Duration elapsed) { | |
_time.value = elapsed.inMicroseconds / 1e6; | |
if (elapsed.inSeconds >= animationDurationInSeconds) { | |
_ticker.stop(); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return CustomPaint( | |
willChange: _ticker.isActive, | |
painter: FunvasPainter( | |
time: _time, | |
delegate: widget.funvas, | |
), | |
); | |
} | |
} | |
/// The custom canvas painter that you need to implement in order to paint | |
/// a funvas. | |
/// | |
/// This funvas can draw on the available area from (0, 0), which is the origin | |
/// at the top left of its global position, to the size provided by the context | |
/// [x] using the canvas [c]. | |
/// All drawing happens in [u]. | |
abstract class Funvas { | |
/// The canvas for the funvas. | |
Canvas get c => _c; | |
late Canvas _c; | |
/// The context for the funvas, providing the available size. | |
FunvasContext get x => _x; | |
late FunvasContext _x; | |
@visibleForTesting | |
set x(FunvasContext context) { | |
assert(() { | |
_x = context; | |
return true; | |
}()); | |
} | |
/// Available size which is a shortcut for `x.size`. | |
Size get s => x.size; | |
/// The update function for the funvas based on time [t]. | |
/// | |
/// In this function, you should execute all the canvas operations based on | |
/// [t], which is the time since the funvas was inserted into the tree in | |
/// seconds. | |
void u(double t); | |
/// Returns the sine of [radians], shorthand for [sin]. | |
double S(double radians) => sin(radians); | |
/// Returns the cosine of [radians], shorthand for [cos]. | |
double C(double radians) => cos(radians); | |
/// Returns the tangent of [radians], shorthand for [tan]. | |
double T(double radians) => tan(radians); | |
/// Returns an RGB(O) color, shorthand for [Color.fromRGBO]. | |
Color R(num r, num g, num b, [num? o]) { | |
return Color.fromRGBO(r ~/ 1, g ~/ 1, b ~/ 1, (o ?? 1).toDouble()); | |
} | |
/// Scales the canvas to a square with side lengths of [dimension]. | |
/// | |
/// Additionally, translate the canvas, so that the square is centered (in | |
/// case the original size does not have an aspect ratio of `1`. | |
/// This means that dimensions will not be distorted (pixel aspect ratio will | |
/// stay the same) and instead, there will be unused space in case the | |
/// original dimensions had a different aspect ratio than `1`. | |
/// Furthermore, to ensure the square effect, the square will be clipped. | |
/// | |
/// The translation and clipping can also be turned off by passing [translate] | |
/// and [clip] as false. | |
/// | |
/// Returns the scaled width and height as a [Size]. If [translate] is `true`, | |
/// the returned size will be a square of the given [dimension]. Otherwise, | |
/// the returned size might have one side that is larger. | |
/// | |
/// ### Use cases | |
/// | |
/// This method is useful if you know that you will be designing a funvas for | |
/// certain dimensions. This way, you can use fixed values for all sizes in | |
/// the animation and when the funvas is displayed in different dimensions, | |
/// your fixed dimensions still work. | |
/// | |
/// I use this a lot because I know what dimensions I want to have for the | |
/// GIF that I will be posting to Twitter beforehand. | |
/// | |
/// ### Notes | |
/// | |
/// "s2q" stands for "scale to square". I decided to not use "s2s" because | |
/// it sounded a bit weird. | |
/// | |
/// --- | |
/// | |
/// My usage recommendation is the following: | |
/// | |
/// ```dart | |
/// final s = s2q(750), w = s.width, h = s.height; | |
/// ``` | |
/// | |
/// You could of course simply use a single variable for the dimensions since | |
/// `w` and `h` will be equal for a square. However, using my way will allow | |
/// you to stay flexible. You could simply disable [translate] later on and/or | |
/// use a different aspect ratio :) | |
Size s2q( | |
double dimension, { | |
bool translate = true, | |
bool clip = true, | |
}) { | |
final shortestSide = min(x.width, x.height); | |
if (translate) { | |
// Center the square. | |
c.translate((x.width - shortestSide) / 2, (x.height - shortestSide) / 2); | |
} | |
final scaling = shortestSide / dimension; | |
c.scale(scaling); | |
final scaledSize = translate | |
? Size.square(dimension) | |
: Size(x.width / scaling, x.height / scaling); | |
if (clip) { | |
c.clipRect(Offset.zero & scaledSize); | |
} | |
return scaledSize; | |
} | |
} | |
/// Context for a [Funvas], providing the available size. | |
@immutable | |
class FunvasContext { | |
/// Constructs a context based on the available size. | |
const FunvasContext(this.size); | |
/// The size available to the funvas. | |
final Size size; | |
/// The width available to the canvas. | |
double get width => size.width; | |
/// The height available to the canvas. | |
double get height => size.height; | |
@override | |
String toString() { | |
return 'FunvasContext($width, $height)'; | |
} | |
} | |
/// The [CustomPainter] implementation that provides the [Funvas] with a canvas | |
/// to draw on. | |
/// | |
/// We could also use a [LeafRenderObjectWidget] instead, however, the | |
/// [CustomPainter] abstraction is already pretty much optimized for our use | |
/// case. | |
class FunvasPainter extends CustomPainter { | |
/// Creates a painter for a funvas using the delegate and time. | |
const FunvasPainter({ | |
required this.time, | |
required this.delegate, | |
}) : super(repaint: time); | |
/// The time managed by the funvas state. | |
final ValueListenable<double> time; | |
/// The funvas delegate that takes care of the actual contextual painting. | |
final Funvas delegate; | |
@override | |
void paint(Canvas canvas, Size size) { | |
canvas.save(); | |
canvas.clipRect(Offset.zero & size); | |
delegate._c = canvas; | |
delegate._x = FunvasContext(size); | |
delegate.u(time.value); | |
canvas.restore(); | |
} | |
@override | |
bool shouldRepaint(FunvasPainter oldDelegate) { | |
return oldDelegate.delegate != delegate; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment