| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math'; |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter/animation.dart'; |
| import 'package:flutter/material.dart'; |
| |
| // Based on https://github.com/eseidelGoogle/bezier_perf/blob/master/lib/main.dart |
| class CubicBezierPage extends StatelessWidget { |
| @override |
| Widget build(BuildContext context) { |
| return Center( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: const <Widget>[ |
| Bezier(Colors.amber, 1.0), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class Bezier extends StatelessWidget { |
| const Bezier(this.color, this.scale, {this.blur = 0.0, this.delay = 0.0}); |
| |
| final Color color; |
| final double scale; |
| final double blur; |
| final double delay; |
| |
| List<PathDetail> _getLogoPath() { |
| final List<PathDetail> paths = <PathDetail>[]; |
| |
| final Path path = Path(); |
| path.moveTo(100.0, 97.0); |
| path.cubicTo(100.0, 97.0, 142.0, 59.0, 169.91, 41.22); |
| path.cubicTo(197.82, 23.44, 249.24, 5.52, 204.67, 85.84); |
| |
| paths.add(PathDetail(path)); |
| |
| // Path 2 |
| final Path bezier2Path = Path(); |
| bezier2Path.moveTo(0.0, 70.55); |
| bezier2Path.cubicTo(0.0, 70.55, 42.0, 31.55, 69.91, 14.77); |
| bezier2Path.cubicTo(97.82, -2.01, 149.24, -20.93, 104.37, 59.39); |
| |
| paths.add(PathDetail(bezier2Path, |
| translate: <double>[29.45, 151.0], rotation: -1.5708)); |
| |
| // Path 3 |
| final Path bezier3Path = Path(); |
| bezier3Path.moveTo(0.0, 69.48); |
| bezier3Path.cubicTo(0.0, 69.48, 44.82, 27.92, 69.91, 13.7); |
| bezier3Path.cubicTo(95.0, -0.52, 149.24, -22.0, 104.37, 58.32); |
| |
| paths.add(PathDetail(bezier3Path, |
| translate: <double>[53.0, 200.48], rotation: -3.14159)); |
| |
| // Path 4 |
| final Path bezier4Path = Path(); |
| bezier4Path.moveTo(0.0, 69.48); |
| bezier4Path.cubicTo(0.0, 69.48, 43.82, 27.92, 69.91, 13.7); |
| bezier4Path.cubicTo(96.0, -0.52, 149.24, -22.0, 104.37, 58.32); |
| |
| paths.add(PathDetail(bezier4Path, |
| translate: <double>[122.48, 77.0], rotation: -4.71239)); |
| |
| return paths; |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Stack(children: <Widget>[ |
| CustomPaint( |
| foregroundPainter: |
| BezierPainter(Colors.grey, 0.0, _getLogoPath(), false), |
| size: const Size(100.0, 100.0), |
| ), |
| AnimatedBezier(color, scale, blur: blur, delay: delay), |
| ]); |
| } |
| } |
| |
| class PathDetail { |
| PathDetail(this.path, {this.translate, this.rotation}); |
| |
| Path path; |
| List<double> translate = <double>[]; |
| double rotation; |
| } |
| |
| class AnimatedBezier extends StatefulWidget { |
| const AnimatedBezier(this.color, this.scale, {this.blur = 0.0, this.delay}); |
| |
| final Color color; |
| final double scale; |
| final double blur; |
| final double delay; |
| |
| @override |
| State createState() => AnimatedBezierState(); |
| } |
| |
| class Point { |
| Point(this.x, this.y); |
| |
| double x; |
| double y; |
| } |
| |
| class AnimatedBezierState extends State<AnimatedBezier> |
| with SingleTickerProviderStateMixin { |
| double scale; |
| AnimationController controller; |
| CurvedAnimation curve; |
| bool isPlaying = false; |
| List<List<Point>> pointList = <List<Point>>[] |
| ..add(<Point>[]) |
| ..add(<Point>[]) |
| ..add(<Point>[]) |
| ..add(<Point>[]); |
| bool isReversed = false; |
| |
| List<PathDetail> _playForward() { |
| final List<PathDetail> paths = <PathDetail>[]; |
| final double t = curve.value; |
| final double b = controller.upperBound; |
| double pX; |
| double pY; |
| |
| final Path path = Path(); |
| |
| if (t < b / 2) { |
| pX = _getCubicPoint(t * 2, 100.0, 100.0, 142.0, 169.91); |
| pY = _getCubicPoint(t * 2, 97.0, 97.0, 59.0, 41.22); |
| pointList[0].add(Point(pX, pY)); |
| } else { |
| pX = _getCubicPoint(t * 2 - b, 169.91, 197.80, 249.24, 204.67); |
| pY = _getCubicPoint(t * 2 - b, 41.22, 23.44, 5.52, 85.84); |
| pointList[0].add(Point(pX, pY)); |
| } |
| |
| path.moveTo(100.0, 97.0); |
| |
| for (Point p in pointList[0]) { |
| path.lineTo(p.x, p.y); |
| } |
| |
| paths.add(PathDetail(path)); |
| |
| // Path 2 |
| final Path bezier2Path = Path(); |
| |
| if (t <= b / 2) { |
| final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 42.0, 69.91); |
| final double pY = _getCubicPoint(t * 2, 70.55, 70.55, 31.55, 14.77); |
| pointList[1].add(Point(pX, pY)); |
| } else { |
| final double pX = _getCubicPoint(t * 2 - b, 69.91, 97.82, 149.24, 104.37); |
| final double pY = _getCubicPoint(t * 2 - b, 14.77, -2.01, -20.93, 59.39); |
| pointList[1].add(Point(pX, pY)); |
| } |
| |
| bezier2Path.moveTo(0.0, 70.55); |
| |
| for (Point p in pointList[1]) { |
| bezier2Path.lineTo(p.x, p.y); |
| } |
| |
| paths.add(PathDetail(bezier2Path, |
| translate: <double>[29.45, 151.0], rotation: -1.5708)); |
| |
| // Path 3 |
| final Path bezier3Path = Path(); |
| if (t <= b / 2) { |
| pX = _getCubicPoint(t * 2, 0.0, 0.0, 44.82, 69.91); |
| pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7); |
| pointList[2].add(Point(pX, pY)); |
| } else { |
| pX = _getCubicPoint(t * 2 - b, 69.91, 95.0, 149.24, 104.37); |
| pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32); |
| pointList[2].add(Point(pX, pY)); |
| } |
| |
| bezier3Path.moveTo(0.0, 69.48); |
| |
| for (Point p in pointList[2]) { |
| bezier3Path.lineTo(p.x, p.y); |
| } |
| |
| paths.add(PathDetail(bezier3Path, |
| translate: <double>[53.0, 200.48], rotation: -3.14159)); |
| |
| // Path 4 |
| final Path bezier4Path = Path(); |
| |
| if (t < b / 2) { |
| final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 43.82, 69.91); |
| final double pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7); |
| pointList[3].add(Point(pX, pY)); |
| } else { |
| final double pX = _getCubicPoint(t * 2 - b, 69.91, 96.0, 149.24, 104.37); |
| final double pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32); |
| pointList[3].add(Point(pX, pY)); |
| } |
| |
| bezier4Path.moveTo(0.0, 69.48); |
| |
| for (Point p in pointList[3]) { |
| bezier4Path.lineTo(p.x, p.y); |
| } |
| |
| paths.add(PathDetail(bezier4Path, |
| translate: <double>[122.48, 77.0], rotation: -4.71239)); |
| |
| return paths; |
| } |
| |
| List<PathDetail> _playReversed() { |
| for (List<Point> list in pointList) { |
| if (list.isNotEmpty) { |
| list.removeLast(); |
| } |
| } |
| |
| final List<Point> points = pointList[0]; |
| final Path path = Path(); |
| |
| path.moveTo(100.0, 97.0); |
| |
| for (Point point in points) { |
| path.lineTo(point.x, point.y); |
| } |
| |
| final Path bezier2Path = Path(); |
| |
| bezier2Path.moveTo(0.0, 70.55); |
| |
| for (Point p in pointList[1]) { |
| bezier2Path.lineTo(p.x, p.y); |
| } |
| |
| final Path bezier3Path = Path(); |
| bezier3Path.moveTo(0.0, 69.48); |
| |
| for (Point p in pointList[2]) { |
| bezier3Path.lineTo(p.x, p.y); |
| } |
| |
| final Path bezier4Path = Path(); |
| |
| bezier4Path.moveTo(0.0, 69.48); |
| |
| for (Point p in pointList[3]) { |
| bezier4Path.lineTo(p.x, p.y); |
| } |
| |
| return <PathDetail>[ |
| PathDetail(path), |
| PathDetail(bezier2Path, translate: <double>[29.45, 151.0], rotation: -1.5708), |
| PathDetail(bezier3Path, |
| translate: <double>[53.0, 200.48], rotation: -3.14159), |
| PathDetail(bezier4Path, translate: <double>[122.48, 77.0], rotation: -4.71239), |
| ]; |
| } |
| |
| List<PathDetail> _getLogoPath() { |
| if (!isReversed) { |
| return _playForward(); |
| } |
| |
| return _playReversed(); |
| } |
| |
| //From http://wiki.roblox.com/index.php?title=File:Beziereq4.png |
| double _getCubicPoint(double t, double p0, double p1, double p2, double p3) { |
| return pow(1 - t, 3) * p0 + |
| 3 * pow(1 - t, 2) * t * p1 + |
| 3 * (1 - t) * pow(t, 2) * p2 + |
| pow(t, 3) * p3; |
| } |
| |
| void playAnimation() { |
| isPlaying = true; |
| isReversed = false; |
| for (List<Point> list in pointList) { |
| list.clear(); |
| } |
| controller.reset(); |
| controller.forward(); |
| } |
| |
| void stopAnimation() { |
| isPlaying = false; |
| controller.stop(); |
| for (List<Point> list in pointList) { |
| list.clear(); |
| } |
| } |
| |
| void reverseAnimation() { |
| isReversed = true; |
| controller.reverse(); |
| } |
| |
| @override |
| void initState() { |
| super.initState(); |
| controller = AnimationController( |
| vsync: this, duration: const Duration(milliseconds: 1000)); |
| curve = CurvedAnimation(parent: controller, curve: Curves.linear) |
| ..addListener(() { |
| setState(() {}); |
| }) |
| ..addStatusListener((AnimationStatus state) { |
| if (state == AnimationStatus.completed) { |
| reverseAnimation(); |
| } else if (state == AnimationStatus.dismissed) { |
| playAnimation(); |
| } |
| }); |
| |
| playAnimation(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return CustomPaint( |
| foregroundPainter: BezierPainter(widget.color, |
| curve.value * widget.blur, _getLogoPath(), isPlaying), |
| size: const Size(100.0, 100.0)); |
| } |
| } |
| |
| class BezierPainter extends CustomPainter { |
| BezierPainter(this.color, this.blur, this.path, this.isPlaying); |
| |
| Color color; |
| final double blur; |
| List<PathDetail> path; |
| bool isPlaying; |
| |
| @override |
| void paint(Canvas canvas, Size size) { |
| final Paint paint = Paint(); |
| paint.strokeWidth = 18.0; |
| paint.style = PaintingStyle.stroke; |
| paint.strokeCap = StrokeCap.round; |
| paint.color = color; |
| canvas.scale(0.5, 0.5); |
| |
| for (int i = 0; i < path.length; i++) { |
| if (path[i].translate != null) { |
| canvas.translate(path[i].translate[0], path[i].translate[1]); |
| } |
| |
| if (path[i].rotation != null) { |
| canvas.rotate(path[i].rotation); |
| } |
| |
| if (blur > 0) { |
| final MaskFilter blur = MaskFilter.blur(BlurStyle.normal, this.blur); |
| paint.maskFilter = blur; |
| canvas.drawPath(path[i].path, paint); |
| } |
| |
| paint.maskFilter = null; |
| canvas.drawPath(path[i].path, paint); |
| } |
| } |
| |
| @override |
| bool shouldRepaint(BezierPainter oldDelegate) => true; |
| |
| @override |
| bool shouldRebuildSemantics(BezierPainter oldDelegate) => false; |
| } |