// MIT License // Copyright (c) 2018 Norbert Kozsir // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // ignore_for_file: invalid_use_of_protected_member import 'dart:math'; import 'package:flutter/material.dart'; typedef ParticleBuilder = Particle Function(int index); abstract class Particle { void paint(Canvas canvas, Size size, double progress, int seed); } class FourRandomSlotParticle extends Particle { final List children; final double relativeDistanceToMiddle; FourRandomSlotParticle({required this.children, this.relativeDistanceToMiddle = 2.0}); @override void paint(Canvas canvas, Size size, double progress, int seed) { Random random = Random(seed); int side = 0; for (Particle particle in children) { PositionedParticle( position: sideToOffset(side, size, random) * relativeDistanceToMiddle, child: particle, ).paint(canvas, size, progress, seed); side++; } } Offset sideToOffset(int side, Size size, Random random) { if (side == 0) { return Offset(-random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2)); } else if (side == 1) { return Offset(random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2)); } else if (side == 2) { return Offset(random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2)); } else if (side == 3) { return Offset(-random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2)); } else { throw Exception(); } } double randomOffset(Random random, int range) { return range / 2 - random.nextInt(range); } } class PoppingCircle extends Particle { final Color color; PoppingCircle({required this.color}); final double radius = 3.0; @override void paint(Canvas canvas, Size size, double progress, seed) { if (progress < 0.5) { canvas.drawCircle( Offset.zero, radius + (progress * 8), Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 5.0 - progress * 2); } else { CircleMirror( numberOfParticles: 4, child: AnimatedPositionedParticle( begin: const Offset(0.0, 5.0), end: const Offset(0.0, 15.0), child: FadingRect( color: color, height: 7.0, width: 2.0, )), initialRotation: pi / 4, ).paint(canvas, size, progress, seed); } } } class Firework extends Particle { @override void paint(Canvas canvas, Size size, double progress, int seed) { FourRandomSlotParticle(children: [ IntervalParticle( interval: const Interval(0.0, 0.5, curve: Curves.easeIn), child: PoppingCircle( color: Colors.deepOrangeAccent, ), ), IntervalParticle( interval: const Interval(0.2, 0.5, curve: Curves.easeIn), child: PoppingCircle( color: Colors.green, ), ), IntervalParticle( interval: const Interval(0.4, 0.8, curve: Curves.easeIn), child: PoppingCircle( color: Colors.indigo, ), ), IntervalParticle( interval: const Interval(0.5, 1.0, curve: Curves.easeIn), child: PoppingCircle( color: Colors.teal, ), ), ]).paint(canvas, size, progress, seed); } } /// Mirrors a given particle around a circle. /// /// When using the default constructor you specify one [Particle], this particle /// is going to be used on its own, this implies that /// all mirrored particles are identical (expect for the rotation around the circle) class CircleMirror extends Particle { final ParticleBuilder particleBuilder; final double initialRotation; final int numberOfParticles; CircleMirror.builder({required this.particleBuilder, required this.initialRotation, required this.numberOfParticles}); CircleMirror({required Particle child, required this.initialRotation, required this.numberOfParticles}) : particleBuilder = ((index) => child); @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.save(); canvas.rotate(initialRotation); for (int i = 0; i < numberOfParticles; i++) { particleBuilder(i).paint(canvas, size, progress, seed); canvas.rotate(pi / (numberOfParticles / 2)); } canvas.restore(); } } /// Mirrors a given particle around a circle. /// /// When using the default constructor you specify one [Particle], this particle /// is going to be used on its own, this implies that /// all mirrored particles are identical (expect for the rotation around the circle) class RectangleMirror extends Particle { final ParticleBuilder particleBuilder; /// Position of the first particle on the rect final double initialDistance; final int numberOfParticles; RectangleMirror.builder({required this.particleBuilder, required this.initialDistance, required this.numberOfParticles}); RectangleMirror({required Particle child, required this.initialDistance, required this.numberOfParticles}) : particleBuilder = ((index) => child); @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.save(); double totalLength = size.width * 2 + size.height * 2; double distanceBetweenParticles = totalLength / numberOfParticles; bool onHorizontalAxis = true; int side = 0; assert((distanceBetweenParticles * numberOfParticles).round() == totalLength.round()); canvas.translate(-size.width / 2, -size.height / 2); double currentDistance = initialDistance; for (int i = 0; i < numberOfParticles; i++) { while (true) { if (onHorizontalAxis ? currentDistance > size.width : currentDistance > size.height) { currentDistance -= onHorizontalAxis ? size.width : size.height; onHorizontalAxis = !onHorizontalAxis; side = (++side) % 4; } else { if (side == 0) { assert(onHorizontalAxis); moveTo(canvas, size, 0, currentDistance, 0.0, () { particleBuilder(i).paint(canvas, size, progress, seed); }); } else if (side == 1) { assert(!onHorizontalAxis); moveTo(canvas, size, 1, size.width, currentDistance, () { particleBuilder(i).paint(canvas, size, progress, seed); }); } else if (side == 2) { assert(onHorizontalAxis); moveTo(canvas, size, 2, size.width - currentDistance, size.height, () { particleBuilder(i).paint(canvas, size, progress, seed); }); } else if (side == 3) { assert(!onHorizontalAxis); moveTo(canvas, size, 3, 0.0, size.height - currentDistance, () { particleBuilder(i).paint(canvas, size, progress, seed); }); } break; } } currentDistance += distanceBetweenParticles; } canvas.restore(); } void moveTo(Canvas canvas, Size size, int side, double x, double y, VoidCallback painter) { canvas.save(); canvas.translate(x, y); canvas.rotate(-atan2(size.width / 2 - x, size.height / 2 - y)); painter(); canvas.restore(); } } /// Offsets a child by a given [Offset] class PositionedParticle extends Particle { PositionedParticle({required this.position, required this.child}); final Particle child; final Offset position; @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.save(); canvas.translate(position.dx, position.dy); child.paint(canvas, size, progress, seed); canvas.restore(); } } /// Animates a childs position based on a Tween class AnimatedPositionedParticle extends Particle { AnimatedPositionedParticle({required Offset begin, required Offset end, required this.child}) : offsetTween = Tween(begin: begin, end: end); final Particle child; final Tween offsetTween; @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.save(); canvas.translate(offsetTween.lerp(progress).dx, offsetTween.lerp(progress).dy); child.paint(canvas, size, progress, seed); canvas.restore(); } } /// Specifies an [Interval] for its child. /// /// Instead of applying a curve the the input parameters of the paint method, /// apply it with this Particle. /// /// If you want you child to only animate from 0.0 - 0.5 (relative), specify an [Interval] with those values. class IntervalParticle extends Particle { final Interval interval; final Particle child; IntervalParticle({required this.child, required this.interval}); @override void paint(Canvas canvas, Size size, double progress, seed) { if (progress < interval.begin || progress > interval.end) return; child.paint(canvas, size, interval.transform(progress), seed); } } /// Does nothing else than holding a list of particles and painting them in that order class CompositeParticle extends Particle { final List children; CompositeParticle({required this.children}); @override void paint(Canvas canvas, Size size, double progress, seed) { for (Particle particle in children) { particle.paint(canvas, size, progress, seed); } } } /// A particle which rotates the child. /// /// Does not animate. class RotationParticle extends Particle { final Particle child; final double rotation; RotationParticle({required this.child, required this.rotation}); @override void paint(Canvas canvas, Size size, double progress, int seed) { canvas.save(); canvas.rotate(rotation); child.paint(canvas, size, progress, seed); canvas.restore(); } } /// A particle which rotates a child along a given [Tween] class AnimatedRotationParticle extends Particle { final Particle child; final Tween rotation; AnimatedRotationParticle({required this.child, required double begin, required double end}) : rotation = Tween(begin: begin, end: end); @override void paint(Canvas canvas, Size size, double progress, int seed) { canvas.save(); canvas.rotate(rotation.lerp(progress)); child.paint(canvas, size, progress, seed); canvas.restore(); } } /// Geometry /// /// These are some basic geometric classes which also fade out as time goes on. /// Each primitive should draw itself at the origin. If the orientation matters it should be directed to the top /// (negative y) /// /// A rectangle which also fades out over time. class FadingRect extends Particle { final Color color; final double width; final double height; FadingRect({required this.color, required this.width, required this.height}); @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), Paint()..color = color.withOpacity(1 - progress)); } } /// A circle which fades out over time class FadingCircle extends Particle { final Color color; final double radius; FadingCircle({required this.color, required this.radius}); @override void paint(Canvas canvas, Size size, double progress, seed) { canvas.drawCircle(Offset.zero, radius, Paint()..color = color.withOpacity(1 - progress)); } } /// A triangle which also fades out over time class FadingTriangle extends Particle { /// This controls the shape of the triangle. /// /// Value between 0 and 1 final double variation; final Color color; /// The size of the base side of the triangle. final double baseSize; /// This is the factor of how much bigger then length than the width is final double heightToBaseFactor; FadingTriangle({required this.variation, required this.color, required this.baseSize, required this.heightToBaseFactor}); @override void paint(Canvas canvas, Size size, double progress, int seed) { Path path = Path(); path.moveTo(0.0, 0.0); path.lineTo(baseSize * variation, baseSize * heightToBaseFactor); path.lineTo(baseSize, 0.0); path.close(); canvas.drawPath(path, Paint()..color = color.withOpacity(1 - progress)); } } /// An ugly looking "snake" /// /// See for yourself class FadingSnake extends Particle { final double width; final double segmentLength; final int segments; final double curvyness; final Color color; FadingSnake({required this.width, required this.segmentLength, required this.segments, required this.curvyness, required this.color}); @override void paint(Canvas canvas, Size size, double progress, int seed) { canvas.save(); canvas.rotate(pi / 6); Path path = Path(); for (int i = 0; i < segments; i++) { path.quadraticBezierTo(curvyness * i, segmentLength * (i + 1), curvyness * (i + 1), segmentLength * (i + 1)); } for (int i = segments - 1; i >= 0; i--) { path.quadraticBezierTo(curvyness * (i + 1), segmentLength * i - curvyness, curvyness * i, segmentLength * i - curvyness); } path.close(); canvas.drawPath(path, Paint()..color = color); canvas.restore(); } }