1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:math'; 6 7import 'package:flutter/widgets.dart'; 8import 'package:flutter/animation.dart'; 9import 'package:flutter/material.dart'; 10 11// Based on https://github.com/eseidelGoogle/bezier_perf/blob/master/lib/main.dart 12class CubicBezierPage extends StatelessWidget { 13 @override 14 Widget build(BuildContext context) { 15 return Center( 16 child: Column( 17 mainAxisAlignment: MainAxisAlignment.center, 18 children: const <Widget>[ 19 Bezier(Colors.amber, 1.0), 20 ], 21 ), 22 ); 23 } 24} 25 26class Bezier extends StatelessWidget { 27 const Bezier(this.color, this.scale, {this.blur = 0.0, this.delay = 0.0}); 28 29 final Color color; 30 final double scale; 31 final double blur; 32 final double delay; 33 34 List<PathDetail> _getLogoPath() { 35 final List<PathDetail> paths = <PathDetail>[]; 36 37 final Path path = Path(); 38 path.moveTo(100.0, 97.0); 39 path.cubicTo(100.0, 97.0, 142.0, 59.0, 169.91, 41.22); 40 path.cubicTo(197.82, 23.44, 249.24, 5.52, 204.67, 85.84); 41 42 paths.add(PathDetail(path)); 43 44 // Path 2 45 final Path bezier2Path = Path(); 46 bezier2Path.moveTo(0.0, 70.55); 47 bezier2Path.cubicTo(0.0, 70.55, 42.0, 31.55, 69.91, 14.77); 48 bezier2Path.cubicTo(97.82, -2.01, 149.24, -20.93, 104.37, 59.39); 49 50 paths.add(PathDetail(bezier2Path, 51 translate: <double>[29.45, 151.0], rotation: -1.5708)); 52 53 // Path 3 54 final Path bezier3Path = Path(); 55 bezier3Path.moveTo(0.0, 69.48); 56 bezier3Path.cubicTo(0.0, 69.48, 44.82, 27.92, 69.91, 13.7); 57 bezier3Path.cubicTo(95.0, -0.52, 149.24, -22.0, 104.37, 58.32); 58 59 paths.add(PathDetail(bezier3Path, 60 translate: <double>[53.0, 200.48], rotation: -3.14159)); 61 62 // Path 4 63 final Path bezier4Path = Path(); 64 bezier4Path.moveTo(0.0, 69.48); 65 bezier4Path.cubicTo(0.0, 69.48, 43.82, 27.92, 69.91, 13.7); 66 bezier4Path.cubicTo(96.0, -0.52, 149.24, -22.0, 104.37, 58.32); 67 68 paths.add(PathDetail(bezier4Path, 69 translate: <double>[122.48, 77.0], rotation: -4.71239)); 70 71 return paths; 72 } 73 74 @override 75 Widget build(BuildContext context) { 76 return Stack(children: <Widget>[ 77 CustomPaint( 78 foregroundPainter: 79 BezierPainter(Colors.grey, 0.0, _getLogoPath(), false), 80 size: const Size(100.0, 100.0), 81 ), 82 AnimatedBezier(color, scale, blur: blur, delay: delay), 83 ]); 84 } 85} 86 87class PathDetail { 88 PathDetail(this.path, {this.translate, this.rotation}); 89 90 Path path; 91 List<double> translate = <double>[]; 92 double rotation; 93} 94 95class AnimatedBezier extends StatefulWidget { 96 const AnimatedBezier(this.color, this.scale, {this.blur = 0.0, this.delay}); 97 98 final Color color; 99 final double scale; 100 final double blur; 101 final double delay; 102 103 @override 104 State createState() => AnimatedBezierState(); 105} 106 107class Point { 108 Point(this.x, this.y); 109 110 double x; 111 double y; 112} 113 114class AnimatedBezierState extends State<AnimatedBezier> 115 with SingleTickerProviderStateMixin { 116 double scale; 117 AnimationController controller; 118 CurvedAnimation curve; 119 bool isPlaying = false; 120 List<List<Point>> pointList = <List<Point>>[ 121 <Point>[], 122 <Point>[], 123 <Point>[], 124 <Point>[], 125 ]; 126 bool isReversed = false; 127 128 List<PathDetail> _playForward() { 129 final List<PathDetail> paths = <PathDetail>[]; 130 final double t = curve.value; 131 final double b = controller.upperBound; 132 double pX; 133 double pY; 134 135 final Path path = Path(); 136 137 if (t < b / 2) { 138 pX = _getCubicPoint(t * 2, 100.0, 100.0, 142.0, 169.91); 139 pY = _getCubicPoint(t * 2, 97.0, 97.0, 59.0, 41.22); 140 pointList[0].add(Point(pX, pY)); 141 } else { 142 pX = _getCubicPoint(t * 2 - b, 169.91, 197.80, 249.24, 204.67); 143 pY = _getCubicPoint(t * 2 - b, 41.22, 23.44, 5.52, 85.84); 144 pointList[0].add(Point(pX, pY)); 145 } 146 147 path.moveTo(100.0, 97.0); 148 149 for (Point p in pointList[0]) { 150 path.lineTo(p.x, p.y); 151 } 152 153 paths.add(PathDetail(path)); 154 155 // Path 2 156 final Path bezier2Path = Path(); 157 158 if (t <= b / 2) { 159 final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 42.0, 69.91); 160 final double pY = _getCubicPoint(t * 2, 70.55, 70.55, 31.55, 14.77); 161 pointList[1].add(Point(pX, pY)); 162 } else { 163 final double pX = _getCubicPoint(t * 2 - b, 69.91, 97.82, 149.24, 104.37); 164 final double pY = _getCubicPoint(t * 2 - b, 14.77, -2.01, -20.93, 59.39); 165 pointList[1].add(Point(pX, pY)); 166 } 167 168 bezier2Path.moveTo(0.0, 70.55); 169 170 for (Point p in pointList[1]) { 171 bezier2Path.lineTo(p.x, p.y); 172 } 173 174 paths.add(PathDetail(bezier2Path, 175 translate: <double>[29.45, 151.0], rotation: -1.5708)); 176 177 // Path 3 178 final Path bezier3Path = Path(); 179 if (t <= b / 2) { 180 pX = _getCubicPoint(t * 2, 0.0, 0.0, 44.82, 69.91); 181 pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7); 182 pointList[2].add(Point(pX, pY)); 183 } else { 184 pX = _getCubicPoint(t * 2 - b, 69.91, 95.0, 149.24, 104.37); 185 pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32); 186 pointList[2].add(Point(pX, pY)); 187 } 188 189 bezier3Path.moveTo(0.0, 69.48); 190 191 for (Point p in pointList[2]) { 192 bezier3Path.lineTo(p.x, p.y); 193 } 194 195 paths.add(PathDetail(bezier3Path, 196 translate: <double>[53.0, 200.48], rotation: -3.14159)); 197 198 // Path 4 199 final Path bezier4Path = Path(); 200 201 if (t < b / 2) { 202 final double pX = _getCubicPoint(t * 2, 0.0, 0.0, 43.82, 69.91); 203 final double pY = _getCubicPoint(t * 2, 69.48, 69.48, 27.92, 13.7); 204 pointList[3].add(Point(pX, pY)); 205 } else { 206 final double pX = _getCubicPoint(t * 2 - b, 69.91, 96.0, 149.24, 104.37); 207 final double pY = _getCubicPoint(t * 2 - b, 13.7, -0.52, -22.0, 58.32); 208 pointList[3].add(Point(pX, pY)); 209 } 210 211 bezier4Path.moveTo(0.0, 69.48); 212 213 for (Point p in pointList[3]) { 214 bezier4Path.lineTo(p.x, p.y); 215 } 216 217 paths.add(PathDetail(bezier4Path, 218 translate: <double>[122.48, 77.0], rotation: -4.71239)); 219 220 return paths; 221 } 222 223 List<PathDetail> _playReversed() { 224 for (List<Point> list in pointList) { 225 if (list.isNotEmpty) { 226 list.removeLast(); 227 } 228 } 229 230 final List<Point> points = pointList[0]; 231 final Path path = Path(); 232 233 path.moveTo(100.0, 97.0); 234 235 for (Point point in points) { 236 path.lineTo(point.x, point.y); 237 } 238 239 final Path bezier2Path = Path(); 240 241 bezier2Path.moveTo(0.0, 70.55); 242 243 for (Point p in pointList[1]) { 244 bezier2Path.lineTo(p.x, p.y); 245 } 246 247 final Path bezier3Path = Path(); 248 bezier3Path.moveTo(0.0, 69.48); 249 250 for (Point p in pointList[2]) { 251 bezier3Path.lineTo(p.x, p.y); 252 } 253 254 final Path bezier4Path = Path(); 255 256 bezier4Path.moveTo(0.0, 69.48); 257 258 for (Point p in pointList[3]) { 259 bezier4Path.lineTo(p.x, p.y); 260 } 261 262 return <PathDetail>[ 263 PathDetail(path), 264 PathDetail(bezier2Path, translate: <double>[29.45, 151.0], rotation: -1.5708), 265 PathDetail(bezier3Path, 266 translate: <double>[53.0, 200.48], rotation: -3.14159), 267 PathDetail(bezier4Path, translate: <double>[122.48, 77.0], rotation: -4.71239), 268 ]; 269 } 270 271 List<PathDetail> _getLogoPath() { 272 if (!isReversed) { 273 return _playForward(); 274 } 275 276 return _playReversed(); 277 } 278 279 //From http://wiki.roblox.com/index.php?title=File:Beziereq4.png 280 double _getCubicPoint(double t, double p0, double p1, double p2, double p3) { 281 return pow(1 - t, 3) * p0 + 282 3 * pow(1 - t, 2) * t * p1 + 283 3 * (1 - t) * pow(t, 2) * p2 + 284 pow(t, 3) * p3; 285 } 286 287 void playAnimation() { 288 isPlaying = true; 289 isReversed = false; 290 for (List<Point> list in pointList) { 291 list.clear(); 292 } 293 controller.reset(); 294 controller.forward(); 295 } 296 297 void stopAnimation() { 298 isPlaying = false; 299 controller.stop(); 300 for (List<Point> list in pointList) { 301 list.clear(); 302 } 303 } 304 305 void reverseAnimation() { 306 isReversed = true; 307 controller.reverse(); 308 } 309 310 @override 311 void initState() { 312 super.initState(); 313 controller = AnimationController( 314 vsync: this, duration: const Duration(milliseconds: 1000)); 315 curve = CurvedAnimation(parent: controller, curve: Curves.linear) 316 ..addListener(() { 317 setState(() {}); 318 }) 319 ..addStatusListener((AnimationStatus state) { 320 if (state == AnimationStatus.completed) { 321 reverseAnimation(); 322 } else if (state == AnimationStatus.dismissed) { 323 playAnimation(); 324 } 325 }); 326 327 playAnimation(); 328 } 329 330 @override 331 void dispose() { 332 controller.dispose(); 333 super.dispose(); 334 } 335 336 @override 337 Widget build(BuildContext context) { 338 return CustomPaint( 339 foregroundPainter: BezierPainter(widget.color, 340 curve.value * widget.blur, _getLogoPath(), isPlaying), 341 size: const Size(100.0, 100.0)); 342 } 343} 344 345class BezierPainter extends CustomPainter { 346 BezierPainter(this.color, this.blur, this.path, this.isPlaying); 347 348 Color color; 349 final double blur; 350 List<PathDetail> path; 351 bool isPlaying; 352 353 @override 354 void paint(Canvas canvas, Size size) { 355 final Paint paint = Paint(); 356 paint.strokeWidth = 18.0; 357 paint.style = PaintingStyle.stroke; 358 paint.strokeCap = StrokeCap.round; 359 paint.color = color; 360 canvas.scale(0.5, 0.5); 361 362 for (int i = 0; i < path.length; i++) { 363 if (path[i].translate != null) { 364 canvas.translate(path[i].translate[0], path[i].translate[1]); 365 } 366 367 if (path[i].rotation != null) { 368 canvas.rotate(path[i].rotation); 369 } 370 371 if (blur > 0) { 372 final MaskFilter blur = MaskFilter.blur(BlurStyle.normal, this.blur); 373 paint.maskFilter = blur; 374 canvas.drawPath(path[i].path, paint); 375 } 376 377 paint.maskFilter = null; 378 canvas.drawPath(path[i].path, paint); 379 } 380 } 381 382 @override 383 bool shouldRepaint(BezierPainter oldDelegate) => true; 384 385 @override 386 bool shouldRebuildSemantics(BezierPainter oldDelegate) => false; 387} 388