Skip to content

Instantly share code, notes, and snippets.

@pietervp
Created July 11, 2022 09:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pietervp/ed14c24ed37676d70275dc150552ed8b to your computer and use it in GitHub Desktop.
Save pietervp/ed14c24ed37676d70275dc150552ed8b to your computer and use it in GitHub Desktop.
Example Animation
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