├── .ci_templates └── flutter_netlify_build.sh ├── .fvm └── fvm_config.json ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── display └── Confetti Screenshot.png ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── lib │ ├── main.dart │ └── performance_test.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── confetti.dart └── src │ ├── confetti.dart │ ├── constants.dart │ ├── enums │ ├── blast_directionality.dart │ └── confetti_controller_state.dart │ ├── helper.dart │ ├── particle.dart │ └── particle_stats.dart ├── melos.yaml ├── pubspec.lock ├── pubspec.yaml └── test └── confetti_test.dart /.ci_templates/flutter_netlify_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Setup flutter 4 | FLUTTER=`which flutter` 5 | if [ $? -eq 0 ] 6 | then 7 | # Flutter is installed 8 | FLUTTER=`which flutter` 9 | else 10 | # Get flutter 11 | git clone https://github.com/flutter/flutter.git 12 | FLUTTER=flutter/bin/flutter 13 | # export PATH="$PATH":"$FLUTTER" 14 | fi 15 | 16 | FLUTTER_CHANNEL=stable 17 | FLUTTER_VERSION=v2.5.1 18 | $FLUTTER channel $FLUTTER_CHANNEL 19 | $FLUTTER version $FLUTTER_VERSION 20 | 21 | # # Setup FVM 22 | # FVM=`which fvm` 23 | # if [ $? -eq 0 ] 24 | # then 25 | # # FVM is installed 26 | # FVM=`which fvm` 27 | # else 28 | # # Get FVC 29 | # echo "Getting fvm" 30 | # $FLUTTER pub global activate fvm 31 | # export PATH="$PATH":"$HOME/.pub-cache/bin" 32 | # FVM=`which fvm` 33 | # fi 34 | 35 | # echo "Installing FVM" 36 | 37 | # $FVM install 38 | 39 | # echo "Running pub get" 40 | 41 | # $FVM flutter pub get 42 | 43 | # cd example 44 | 45 | # echo "Building Flutter web" 46 | 47 | # $FVM flutter build web --web-renderer canvaskit 48 | 49 | # cd .. 50 | 51 | cd example 52 | 53 | $FLUTTER build web --web-renderer canvaskit 54 | 55 | cd .. 56 | 57 | echo "OK" -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "2.5.1", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | android/ 3 | ios/ 4 | windows/ 5 | macos/ 6 | linux/ 7 | web/ 8 | 9 | flutter_sdk 10 | .vscode 11 | 12 | # Miscellaneous 13 | *.class 14 | *.log 15 | *.pyc 16 | *.swp 17 | .DS_Store 18 | .atom/ 19 | .buildlog/ 20 | .history 21 | .svn/ 22 | 23 | # IntelliJ related 24 | *.iml 25 | *.ipr 26 | *.iws 27 | .idea/ 28 | 29 | # The .vscode folder contains launch configuration and tasks you configure in 30 | # VS Code which you may wish to be included in version control, so this line 31 | # is commented out by default. 32 | #.vscode/ 33 | 34 | # Flutter/Dart/Pub related 35 | **/doc/api/ 36 | .dart_tool/ 37 | .flutter-plugins 38 | .packages 39 | .pub-cache/ 40 | .pub/ 41 | build/ 42 | 43 | # Android related 44 | **/android/**/gradle-wrapper.jar 45 | **/android/.gradle 46 | **/android/captures/ 47 | **/android/gradlew 48 | **/android/gradlew.bat 49 | **/android/local.properties 50 | **/android/**/GeneratedPluginRegistrant.java 51 | 52 | # iOS/XCode related 53 | **/ios/**/*.mode1v3 54 | **/ios/**/*.mode2v3 55 | **/ios/**/*.moved-aside 56 | **/ios/**/*.pbxuser 57 | **/ios/**/*.perspectivev3 58 | **/ios/**/*sync/ 59 | **/ios/**/.sconsign.dblite 60 | **/ios/**/.tags* 61 | **/ios/**/.vagrant/ 62 | **/ios/**/DerivedData/ 63 | **/ios/**/Icon? 64 | **/ios/**/Pods/ 65 | **/ios/**/.symlinks/ 66 | **/ios/**/profile 67 | **/ios/**/xcuserdata 68 | **/ios/.generated/ 69 | **/ios/Flutter/App.framework 70 | **/ios/Flutter/Flutter.framework 71 | **/ios/Flutter/Generated.xcconfig 72 | **/ios/Flutter/app.flx 73 | **/ios/Flutter/app.zip 74 | **/ios/Flutter/flutter_assets/ 75 | **/ios/ServiceDefinitions.json 76 | **/ios/Runner/GeneratedPluginRegistrant.* 77 | 78 | # Exceptions to above rules. 79 | !**/ios/**/default.mode1v3 80 | !**/ios/**/default.mode2v3 81 | !**/ios/**/default.pbxuser 82 | !**/ios/**/default.perspectivev3 83 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 84 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Upcomming 2 | ⛔️ Breaking! 3 | - Added the concept of delta time, to allow for better simulation when the refresh rate is 120 (or more than 60). There should be no noticable change on a 60fps screen, however, there may be variance if Flutter drops frames. No changes needed from a widget level. 4 | - `ParticleSystem.update` and `Pariticle.update` now contain a `deltaTime` argument. No changes needed from a widget level. 5 | - Some visual difference, as now not all particles will rotate on the z-axis (50% chance). This change was maded to enhance visual fidelity. 6 | 7 | ⭐️ Added 8 | - `clearAllParticles` added to stop method on controller: `_confettiController.stop(clearAllParticles: true);` default is false. If true particles will immediately be cleared/removed on stop. Calling `dispose` will also clear all particles immediately. 9 | - `particleStatsCallback` added to `Confetti` widget to retrieve the `ParticleStats`. This provides info on the particle system, such as number of active and number of total particles in memory. 10 | - `pauseEmissionOnLowFrameRate` to `Confetti` widget. Default is true. This will pause additional confetti emission if the frame rate is below 60fps, and will continue when resources are available. This can be disabled by setting to `false`, to force particle creation regardless of frame rate. 11 | 12 | ⚡️ Improved 13 | - Various performance improvements! 14 | - Confetti is now conditionally reused instead of recreated, big improvement 15 | - 120hz refresh rate supported 16 | - `pauseEmissionOnLowFrameRate` boolean added to `Confetti` widget to ensure smooth 60 FPS. This may however result in no confetti appearing if other complex operations are taking up resources. Set to `false` to disable. 17 | - Temporary fix for issue [[#66](https://github.com/funwithflutter/flutter_confetti/issues/66)] with severe perf issues on Chrome macOS. 18 | 19 | ## [0.7.0] 20 | ⭐️ Added 21 | - Stroke width and color can now optionally be set. `strokeWidth` (default 0) and `strokeColor` (default black). Requires a stroke width bigger than 0 22 | - Updated to Flutter 3.0 23 | 24 | ## [0.6.0] 25 | 🔄 Changed 26 | - Removed the random_color package and replaced with custom logic. Random colors may now be slightly different. 27 | - Updated dependencies 28 | 29 | 🐞 Fixed 30 | - Unmounted exception (https://github.com/funwithflutter/flutter_confetti/issues/36). Thanks Iiropel. 31 | - Moved `.super` call to the top of `initState`. 32 | 33 | ## [0.6.0-nullsafety] 34 | Now with null safety :) - Thanks Ali1Ammar! 35 | 36 | ## [0.5.5] 37 | Add optional `createParticlePath` function to pass in a custom `Path` for the conveti (for example a Star path, instead of the default Rectangle path). Example updated. Thanks Artur-Wisniewski. 38 | Fix: Animation stop event not firing. Thanks WieFel. 39 | 40 | ## [0.5.4+1] 41 | Fix: Call play on the confetti controller from `initState`. 42 | 43 | ## [0.5.4] 44 | Fix: Confetti emitter position set incorrectly when transitioning to a new PageView. The emitter position is now set on animation start. 45 | Fix: Set `ConfettiControllerState.stopped` on `ConfettiWidget` dispose. 46 | 47 | ## [0.5.3] 48 | Add `canvas` parameter. 49 | 50 | ## [0.5.2] 51 | Fix where at certain times the Confetti widget takes too long to emit. This update ensures that particles are generated on the first frame, and when there are no longer any particles on the screen but the animation is still running. 52 | 53 | ## [0.5.1] 54 | Fixed layout issue where the screen size and confetti position were not updated on layout changes. The package will now respond to screen layout and sizing changes. 55 | 56 | ## [0.5.0] 57 | Massive performance improvements. Should see a significant performance boost when running the application in profile/release mode. It is now possible to add a lot more confetti without the application causing jank. It is recommended to test the use of this package on multiple devices, to ensure it does not introduce performance issues on older devices. 58 | 59 | 60 | ## [0.4.0] 61 | This update will result in a change in the default falling speed (gravity) and drag of the confetti. You may note a difference, and might be required to modify some of these paramaters to achieve the desired result 62 | 63 | * Added an optional `gravity` to change the speed at which the confetti falls 64 | * Added an optional `blastDirectionality` property. The default is `BlastDirectionality.directional` where you can specify a `blastDirection` to shoot the confetti in a specific direction. Change to `BlastDirectionality.explosive` to blast confetti in random directions 65 | * Added an optional `particleDrag` property to configure the drag to apply to the confetti 66 | 67 | ## [0.3.0] 68 | * Provide an optional `minimumSize` and `maximumSize` to customize the size of the confetti. For example, setting a `minimumSize` equal to `Size(10,10)` and a `maximumSize` equal to `Size(20,20)` will create confetti with a size between these two parameters. Can be provided as an argument in the `ConfettiWidget` 69 | 70 | ## [0.2.0] 71 | 72 | * Provide an optional Color List to specify specific colors for the confetti. A single color, for example `[Colors.blue]`, or multiple colors `[Colors.blue, Colors.red, Colors.green]` can be provided as an argument in the `ConfettiWidget` 73 | 74 | ## [0.1.2] 75 | 76 | * Provide optional child widget to render below the confetti 77 | * Changed the painter to use foregroundPainter to always paint the confetti above its child 78 | 79 | ## [0.1.1] 80 | 81 | * Patch null pointer exception 82 | 83 | ## [0.1.0] 84 | 85 | * Initial release. You will probably experience some performance issues if you try and create too many particles at once 86 | * Performance optimization work will be done in later versions. 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2018 Felix Angelov 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without restriction, 8 | including without limitation the rights to use, copy, modify, merge, 9 | publish, distribute, sublicense, and/or sell copies of the Software, 10 | and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 21 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 22 | USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Blast some confetti all over the screen and celebrate user achievements! 2 | 3 | 4 | 5 | ## Demo 6 | 7 | Video showing the Confetti in Action: https://youtu.be/dGMVyUY4-6M 8 | 9 | Live [WEB Demo](https://funwithflutter.github.io/confetti/#/) 10 | 11 | An __old__ video walkthrough is available [here](https://www.youtube.com/watch?v=jvhw3cfj2rk). 12 | 13 | 14 | ## Getting Started 15 | 16 | To use this plugin, add `confetti` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). 17 | 18 | See the example to get started quickly. To generate the platform folders run: 19 | 20 | ```dart 21 | flutter create . 22 | ``` 23 | 24 | in the example folder. 25 | 26 | To begin you need to instantiate a `ConfettiController` variable and pass in a `Duration` argument. The `ConfettiController` can be instantiated in the `initState` method and disposed in the `dispose` method. 27 | 28 | In the `build` method return a `ConfettiWidget`. The only attribute that is required is the `ConfettiController`. 29 | 30 | Other attributes that can be set are: 31 | * `blastDirectionality` -> an enum value to state if the particles shoot in random directions or a specific direction. `BlastDirectionality.explosive` will shoot in random directions and don't require a `blastDirection` to be set. `BlastDirectionality.directional` requires a `blastDirection` to specify the direction of the confetti. 32 | * `blastDirection` -> a radial value to determine the direction of the particle emission. The default is set to `PI` (180 degrees). A value of `PI` will emit to the left of the canvas/screen. 33 | * `emissionFrequency` -> should be a value between 0 and 1. The higher the value the higher the likelihood that particles will be emitted on a single frame. Default is set to `0.02` (2% chance) 34 | * `numberOfParticles` -> the number of particles to be emitted per emission. Default is set to `10` 35 | * `shouldLoop` -> determines if the emission will reset after the duration is completed, which will result in continues particles being emitted, and the animation looping 36 | * `maxBlastForce` -> will determine the maximum blast force applied to a particle within it's first 5 frames of life. The default `maxBlastForce` is set to `20` 37 | * `minBlastForce` -> will determine the minimum blast force applied to a particle within it's first 5 frames of life. The default `minBlastForce` is set to `5` 38 | * `displayTarget` -> if `true` a crosshair will be displayed to show the location of the particle emitter 39 | * `colors` -> a list of colors can be provided to manually set the confetti colors. If omitted then random colors will be used. A single color, for example `[Colors.blue]`, or multiple colors `[Colors.blue, Colors.red, Colors.green]` can be provided as an argument in the `ConfettiWidget 40 | * `strokeWidth` optionally set to give a stroke to the paint. Needs to be bigger than 0 to be vissible. Default 0. 41 | * `strokeColor` optionally set to give a stroke color. Default black. 42 | * `minimumSize` -> a `Size` controlling the minimum possible size of the confetti. To be used in conjuction with `maximumSize`. For example, setting a `minimumSize` equal to `Size(10,10)` will ensure that the confetti will never be smaller than the specified size. Must be positive and smaller than the `maximumSize`. Can not be null. 43 | * `maximumSize` -> a `Size` controlling the maximum possible size of the confetti. To be used in conjuction with `minimumSize`. For example, setting a `maximumSize` equal to `Size(100,100)` will create confetti with a size somewhere between the minimum and maximum size of (100, 100) [widht, height]. Must be positive and bigger than the `minimumSize`, Can not be null. 44 | * `gravity` -> change the speed at which the confetti falls. A value between 0 and 1. The higher the value the faster it will fall. Default is set to `0.1` 45 | * `particleDrag` -> configure the drag force to apply to the confetti. A value between 0 and 1. A value of 1 will be no drag at all, while 0.1, for example, will be a lot of drag. Default is set to `0.05` 46 | * `canvas` -> set the size of the area where the confetti will be shown, by default this is set to full screen size. 47 | * `createParticlePath` -> An optional function that retuns a custom `Path` to generate unique particles. Default returns a rectangular path. 48 | 49 | 50 | ### Example of a custom `createParticlePath` 51 | 52 | ```dart 53 | Path drawStar(Size size) { 54 | // Method to convert degree to radians 55 | double degToRad(double deg) => deg * (pi / 180.0); 56 | 57 | const numberOfPoints = 5; 58 | final halfWidth = size.width / 2; 59 | final externalRadius = halfWidth; 60 | final internalRadius = halfWidth / 2.5; 61 | final degreesPerStep = degToRad(360 / numberOfPoints); 62 | final halfDegreesPerStep = degreesPerStep / 2; 63 | final path = Path(); 64 | final fullAngle = degToRad(360); 65 | path.moveTo(size.width, halfWidth); 66 | 67 | for (double step = 0; step < fullAngle; step += degreesPerStep) { 68 | path.lineTo(halfWidth + externalRadius * cos(step), 69 | halfWidth + externalRadius * sin(step)); 70 | path.lineTo(halfWidth + internalRadius * cos(step + halfDegreesPerStep), 71 | halfWidth + internalRadius * sin(step + halfDegreesPerStep)); 72 | } 73 | path.close(); 74 | return path; 75 | } 76 | ``` 77 | 78 | Enjoy the confetti. 79 | 80 | *NOTE:* Don't be greedy with the number of particles. The more particles that are on screen the more calculations need to be performed. Performance improvements have been made, however this is still ongoing work. Too many particles will result in performance issues. Use wisely and carefully. 81 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /display/Confetti Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/flutter_confetti/736dd344a4f6a9d38a45d937964544e1bb741c7e/display/Confetti Screenshot.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | android/ 3 | ios/ 4 | windows/ 5 | macos/ 6 | linux/ 7 | web/ 8 | 9 | fvm_config.json 10 | 11 | # Miscellaneous 12 | *.class 13 | *.log 14 | *.pyc 15 | *.swp 16 | .DS_Store 17 | .atom/ 18 | .buildlog/ 19 | .history 20 | .svn/ 21 | 22 | # IntelliJ related 23 | *.iml 24 | *.ipr 25 | *.iws 26 | .idea/ 27 | 28 | # The .vscode folder contains launch configuration and tasks you configure in 29 | # VS Code which you may wish to be included in version control, so this line 30 | # is commented out by default. 31 | #.vscode/ 32 | 33 | # Flutter/Dart/Pub related 34 | **/doc/api/ 35 | .dart_tool/ 36 | .flutter-plugins 37 | .packages 38 | .pub-cache/ 39 | .pub/ 40 | /build/ 41 | 42 | # Web related 43 | 44 | # Exceptions to above rules. 45 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 46 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 17 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 18 | - platform: android 19 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 20 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 21 | - platform: ios 22 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 23 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 24 | - platform: linux 25 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 26 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 27 | - platform: macos 28 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 29 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 30 | - platform: web 31 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 32 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 33 | - platform: windows 34 | create_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 35 | base_revision: b8f7f1f9869bb2d116aa6a70dbeac61000b52849 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funwithflutter/flutter_confetti/736dd344a4f6a9d38a45d937964544e1bb741c7e/example/analysis_options.yaml -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:confetti/confetti.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() => runApp(const ConfettiSample()); 7 | 8 | class ConfettiSample extends StatelessWidget { 9 | const ConfettiSample({Key? key}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) => MaterialApp( 13 | title: 'Confetti', 14 | home: Scaffold( 15 | backgroundColor: Colors.grey[900], 16 | body: MyApp(), 17 | ), 18 | ); 19 | } 20 | 21 | class MyApp extends StatefulWidget { 22 | @override 23 | _MyAppState createState() => _MyAppState(); 24 | } 25 | 26 | class _MyAppState extends State { 27 | late ConfettiController _controllerCenter; 28 | late ConfettiController _controllerCenterRight; 29 | late ConfettiController _controllerCenterLeft; 30 | late ConfettiController _controllerTopCenter; 31 | late ConfettiController _controllerBottomCenter; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | _controllerCenter = 37 | ConfettiController(duration: const Duration(seconds: 10)); 38 | _controllerCenterRight = 39 | ConfettiController(duration: const Duration(seconds: 10)); 40 | _controllerCenterLeft = 41 | ConfettiController(duration: const Duration(seconds: 10)); 42 | _controllerTopCenter = 43 | ConfettiController(duration: const Duration(seconds: 10)); 44 | _controllerBottomCenter = 45 | ConfettiController(duration: const Duration(seconds: 10)); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | _controllerCenter.dispose(); 51 | _controllerCenterRight.dispose(); 52 | _controllerCenterLeft.dispose(); 53 | _controllerTopCenter.dispose(); 54 | _controllerBottomCenter.dispose(); 55 | 56 | super.dispose(); 57 | } 58 | 59 | /// A custom Path to paint stars. 60 | Path drawStar(Size size) { 61 | // Method to convert degrees to radians 62 | double degToRad(double deg) => deg * (pi / 180.0); 63 | 64 | const numberOfPoints = 5; 65 | final halfWidth = size.width / 2; 66 | final externalRadius = halfWidth; 67 | final internalRadius = halfWidth / 2.5; 68 | final degreesPerStep = degToRad(360 / numberOfPoints); 69 | final halfDegreesPerStep = degreesPerStep / 2; 70 | final path = Path(); 71 | final fullAngle = degToRad(360); 72 | path.moveTo(size.width, halfWidth); 73 | 74 | for (double step = 0; step < fullAngle; step += degreesPerStep) { 75 | path.lineTo(halfWidth + externalRadius * cos(step), 76 | halfWidth + externalRadius * sin(step)); 77 | path.lineTo(halfWidth + internalRadius * cos(step + halfDegreesPerStep), 78 | halfWidth + internalRadius * sin(step + halfDegreesPerStep)); 79 | } 80 | path.close(); 81 | return path; 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return SafeArea( 87 | child: Stack( 88 | children: [ 89 | //CENTER -- Blast 90 | Align( 91 | alignment: Alignment.center, 92 | child: ConfettiWidget( 93 | confettiController: _controllerCenter, 94 | blastDirectionality: BlastDirectionality 95 | .explosive, // don't specify a direction, blast randomly 96 | shouldLoop: 97 | true, // start again as soon as the animation is finished 98 | colors: const [ 99 | Colors.green, 100 | Colors.blue, 101 | Colors.pink, 102 | Colors.orange, 103 | Colors.purple 104 | ], // manually specify the colors to be used 105 | createParticlePath: drawStar, // define a custom shape/path. 106 | ), 107 | ), 108 | Align( 109 | alignment: Alignment.center, 110 | child: TextButton( 111 | onPressed: () { 112 | _controllerCenter.play(); 113 | }, 114 | child: _text('blast\nstars'), 115 | ), 116 | ), 117 | 118 | //CENTER RIGHT -- Emit left 119 | Align( 120 | alignment: Alignment.centerRight, 121 | child: ConfettiWidget( 122 | confettiController: _controllerCenterRight, 123 | blastDirection: pi, // radial value - LEFT 124 | particleDrag: 0.05, // apply drag to the confetti 125 | emissionFrequency: 0.05, // how often it should emit 126 | numberOfParticles: 20, // number of particles to emit 127 | gravity: 0.05, // gravity - or fall speed 128 | shouldLoop: false, 129 | colors: const [ 130 | Colors.green, 131 | Colors.blue, 132 | Colors.pink 133 | ], // manually specify the colors to be used 134 | strokeWidth: 1, 135 | strokeColor: Colors.white, 136 | ), 137 | ), 138 | Align( 139 | alignment: Alignment.centerRight, 140 | child: TextButton( 141 | onPressed: () { 142 | _controllerCenterRight.play(); 143 | }, 144 | child: _text('pump left'), 145 | ), 146 | ), 147 | 148 | //CENTER LEFT - Emit right 149 | Align( 150 | alignment: Alignment.centerLeft, 151 | child: ConfettiWidget( 152 | confettiController: _controllerCenterLeft, 153 | blastDirection: 0, // radial value - RIGHT 154 | emissionFrequency: 0.6, 155 | // set the minimum potential size for the confetti (width, height) 156 | minimumSize: const Size(10, 10), 157 | // set the maximum potential size for the confetti (width, height) 158 | maximumSize: const Size(50, 50), 159 | numberOfParticles: 1, 160 | gravity: 0.1, 161 | ), 162 | ), 163 | Align( 164 | alignment: Alignment.centerLeft, 165 | child: TextButton( 166 | onPressed: () { 167 | _controllerCenterLeft.play(); 168 | }, 169 | child: _text('singles'), 170 | ), 171 | ), 172 | 173 | //TOP CENTER - shoot down 174 | Align( 175 | alignment: Alignment.topCenter, 176 | child: ConfettiWidget( 177 | confettiController: _controllerTopCenter, 178 | blastDirection: pi / 2, 179 | maxBlastForce: 5, // set a lower max blast force 180 | minBlastForce: 2, // set a lower min blast force 181 | emissionFrequency: 0.05, 182 | numberOfParticles: 50, // a lot of particles at once 183 | gravity: 1, 184 | ), 185 | ), 186 | Align( 187 | alignment: Alignment.topCenter, 188 | child: TextButton( 189 | onPressed: () { 190 | _controllerTopCenter.play(); 191 | }, 192 | child: _text('goliath')), 193 | ), 194 | //BOTTOM CENTER - Shoot up 195 | Align( 196 | alignment: Alignment.bottomCenter, 197 | child: ConfettiWidget( 198 | confettiController: _controllerBottomCenter, 199 | blastDirection: -pi / 2, 200 | emissionFrequency: 0.01, 201 | numberOfParticles: 20, 202 | maxBlastForce: 100, 203 | minBlastForce: 80, 204 | gravity: 0.3, 205 | ), 206 | ), 207 | Align( 208 | alignment: Alignment.bottomCenter, 209 | child: TextButton( 210 | onPressed: () { 211 | _controllerBottomCenter.play(); 212 | }, 213 | child: _text('hard and infrequent'), 214 | ), 215 | ), 216 | ], 217 | ), 218 | ); 219 | } 220 | 221 | Text _text(String text) => Text( 222 | text, 223 | style: const TextStyle(color: Colors.white, fontSize: 20), 224 | ); 225 | } 226 | -------------------------------------------------------------------------------- /example/lib/performance_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:confetti/confetti.dart'; 6 | 7 | void main() => runApp(const ConfettiPerformanceTestSample()); 8 | 9 | class ConfettiPerformanceTestSample extends StatelessWidget { 10 | const ConfettiPerformanceTestSample({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) => MaterialApp( 14 | title: 'Confetti Performance Test', 15 | showPerformanceOverlay: true, 16 | home: Scaffold( 17 | backgroundColor: Colors.grey[900], 18 | body: MyApp(), 19 | ), 20 | ); 21 | } 22 | 23 | class MyApp extends StatefulWidget { 24 | @override 25 | _MyAppState createState() => _MyAppState(); 26 | } 27 | 28 | class _MyAppState extends State { 29 | late ConfettiController _confettiController; 30 | 31 | final ConfettiStats stats = ConfettiStats(); 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | _confettiController = ConfettiController( 37 | duration: const Duration(seconds: 10), 38 | 39 | /// The following can be used to retrieve stats on the particles. 40 | particleStatsCallback: (pStats) => stats.setStats(pStats), 41 | ); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | _confettiController.dispose(); 47 | super.dispose(); 48 | } 49 | 50 | /// A custom Path to paint stars. 51 | Path drawStar(Size size) { 52 | // Method to convert degrees to radians 53 | double degToRad(double deg) => deg * (pi / 180.0); 54 | 55 | const numberOfPoints = 5; 56 | final halfWidth = size.width / 2; 57 | final externalRadius = halfWidth; 58 | final internalRadius = halfWidth / 2.5; 59 | final degreesPerStep = degToRad(360 / numberOfPoints); 60 | final halfDegreesPerStep = degreesPerStep / 2; 61 | final path = Path(); 62 | final fullAngle = degToRad(360); 63 | path.moveTo(size.width, halfWidth); 64 | 65 | for (double step = 0; step < fullAngle; step += degreesPerStep) { 66 | path.lineTo(halfWidth + externalRadius * cos(step), 67 | halfWidth + externalRadius * sin(step)); 68 | path.lineTo(halfWidth + internalRadius * cos(step + halfDegreesPerStep), 69 | halfWidth + internalRadius * sin(step + halfDegreesPerStep)); 70 | } 71 | path.close(); 72 | return path; 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return SafeArea( 78 | child: Stack( 79 | children: [ 80 | Align( 81 | alignment: Alignment.center, 82 | child: ConfettiWidget( 83 | emissionFrequency: 1, 84 | numberOfParticles: 100, 85 | confettiController: _confettiController, 86 | blastDirectionality: BlastDirectionality.explosive, 87 | shouldLoop: true, 88 | createParticlePath: drawStar, 89 | ), 90 | ), 91 | Align( 92 | alignment: Alignment.center, 93 | child: Row( 94 | mainAxisSize: MainAxisSize.min, 95 | children: [ 96 | TextButton( 97 | onPressed: () { 98 | _confettiController.play(); 99 | }, 100 | child: _text('start'), 101 | ), 102 | TextButton( 103 | onPressed: () { 104 | _confettiController.stop(); 105 | }, 106 | child: _text('stop'), 107 | ), 108 | TextButton( 109 | onPressed: () { 110 | _confettiController.dispose(); 111 | }, 112 | child: _text('dispose'), 113 | ), 114 | ], 115 | ), 116 | ), 117 | 118 | /// Display stats of confetti 119 | Align( 120 | alignment: Alignment.topCenter, 121 | child: AnimatedBuilder( 122 | animation: stats, 123 | builder: (context, _) => Column( 124 | children: [ 125 | Text( 126 | 'Particles: ${stats.stats.numberOfParticles}', 127 | style: TextStyle(color: Colors.white, fontSize: 22), 128 | ), 129 | Text( 130 | 'Active Particles: ${stats.stats.activeNumberOfParticles}', 131 | style: TextStyle(color: Colors.white, fontSize: 22), 132 | ), 133 | ], 134 | ), 135 | ), 136 | ), 137 | ], 138 | ), 139 | ); 140 | } 141 | 142 | Text _text(String text) => Text( 143 | text, 144 | style: const TextStyle(color: Colors.white, fontSize: 20), 145 | ); 146 | } 147 | 148 | /// Demonstration showing how to use the `particleStatsCallback` to retrieve 149 | /// [ParticleStats]. 150 | class ConfettiStats extends ChangeNotifier { 151 | ParticleStats stats; 152 | ConfettiStats() : stats = ParticleStats.empty(); 153 | 154 | void setStats(ParticleStats value) { 155 | stats = value; 156 | notifyListeners(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | confetti: 40 | dependency: "direct main" 41 | description: 42 | path: ".." 43 | relative: true 44 | source: path 45 | version: "0.7.0" 46 | cupertino_icons: 47 | dependency: "direct main" 48 | description: 49 | name: cupertino_icons 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.1.2" 53 | fake_async: 54 | dependency: transitive 55 | description: 56 | name: fake_async 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.12" 77 | material_color_utilities: 78 | dependency: transitive 79 | description: 80 | name: material_color_utilities 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.1.5" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.2" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.9.0" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.1" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.1" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.4.12" 145 | vector_math: 146 | dependency: transitive 147 | description: 148 | name: vector_math 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.1.2" 152 | sdks: 153 | dart: ">=2.17.0 <3.0.0" 154 | flutter: ">=3.0.0" 155 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | version: 1.0.0+1 5 | 6 | publish_to: none 7 | 8 | environment: 9 | sdk: ">=2.17.0 <3.0.0" 10 | 11 | dependencies: 12 | confetti: 13 | path: ../ 14 | cupertino_icons: ^0.1.2 15 | flutter: 16 | sdk: flutter 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | flutter: 23 | uses-material-design: true 24 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/confetti.dart: -------------------------------------------------------------------------------- 1 | library confetti; 2 | 3 | export 'src/confetti.dart'; 4 | export 'src/enums/blast_directionality.dart'; 5 | export 'src/enums/confetti_controller_state.dart'; 6 | export 'src/particle_stats.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/confetti.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:confetti/src/particle_stats.dart'; 4 | import 'package:confetti/src/constants.dart'; 5 | import 'package:confetti/src/particle.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | import 'enums/blast_directionality.dart'; 9 | import 'enums/confetti_controller_state.dart'; 10 | 11 | class ConfettiWidget extends StatefulWidget { 12 | const ConfettiWidget({ 13 | Key? key, 14 | required this.confettiController, 15 | this.emissionFrequency = 0.02, 16 | this.numberOfParticles = 10, 17 | this.maxBlastForce = 20, 18 | this.minBlastForce = 5, 19 | this.blastDirectionality = BlastDirectionality.directional, 20 | this.blastDirection = pi, 21 | this.gravity = 0.2, 22 | this.shouldLoop = false, 23 | this.displayTarget = false, 24 | this.colors, 25 | this.strokeColor = Colors.black, 26 | this.strokeWidth = 0, 27 | this.minimumSize = const Size(20, 10), 28 | this.maximumSize = const Size(30, 15), 29 | this.particleDrag = 0.05, 30 | this.canvas, 31 | this.pauseEmissionOnLowFrameRate = true, 32 | this.createParticlePath, 33 | this.child, 34 | }) : assert( 35 | emissionFrequency >= 0 && 36 | emissionFrequency <= 1 && 37 | numberOfParticles > 0 && 38 | maxBlastForce > 0 && 39 | minBlastForce > 0 && 40 | maxBlastForce > minBlastForce, 41 | ), 42 | assert(gravity >= 0 && gravity <= 1, 43 | '`gravity` needs to be between 0 and 1'), 44 | assert(strokeWidth >= 0, '`strokeWidth needs to be bigger than 0'), 45 | super(key: key); 46 | 47 | /// Controls the animation. 48 | final ConfettiController confettiController; 49 | 50 | /// The [maxBlastForce] and [minBlastForce] will determine the maximum and 51 | /// minimum blast force applied to a particle within it's first 5 frames of 52 | /// life. The default [maxBlastForce] is set to `20` 53 | final double maxBlastForce; 54 | 55 | /// The [maxBlastForce] and [minBlastForce] will determine the maximum and 56 | /// minimum blast force applied to a particle within it's first 5 frames of 57 | /// life. The default [minBlastForce] is set to `5` 58 | final double minBlastForce; 59 | 60 | /// {@macro blast_directionality} 61 | /// 62 | /// The default value is [BlastDirectionality.directional], the direction 63 | /// can be set with [blastDirection]. 64 | final BlastDirectionality blastDirectionality; 65 | 66 | /// The [blastDirection] is a radial value to determine the direction of the 67 | /// particle emission. 68 | /// 69 | /// The default is set to `PI` (180 degrees). 70 | /// A value of `PI` will emit to the left of the canvas/screen. 71 | final double blastDirection; 72 | 73 | /// The [createParticlePath] is an optional function that returns a custom 74 | /// `Path` to generate particles. 75 | /// 76 | /// The default function returns a rectangular path. 77 | final Path Function(Size size)? createParticlePath; 78 | 79 | /// The [gravity] is the speed at which the confetti will fall. 80 | /// The higher the [gravity] the faster it will fall. 81 | /// 82 | /// It can be set to a value between `0` and `1` 83 | /// 84 | /// Default value is `0.1` 85 | final double gravity; 86 | 87 | /// The [emissionFrequency] should be a value between 0 and 1. 88 | /// The higher the value the higher the likelihood that particles will be 89 | /// emitted on a single frame. 90 | /// 91 | /// Default is set to `0.02` (2% chance). 92 | final double emissionFrequency; 93 | 94 | /// The [numberOfParticles] to be emitted per emission. 95 | /// 96 | /// Default is set to `10`. 97 | final int numberOfParticles; 98 | 99 | /// The [shouldLoop] attribute determines if the animation will 100 | /// reset once it completes, resulting in a continuous particle emission. 101 | final bool shouldLoop; 102 | 103 | /// The [displayTarget] attribute determines if a crosshair will be displayed 104 | /// to show the location of the particle emitter. 105 | final bool displayTarget; 106 | 107 | /// List of Colors to iterate over - if null then random values will be chosen 108 | final List? colors; 109 | 110 | /// Stroke width of the confetti (0.0 by default, no stroke) 111 | final double strokeWidth; 112 | 113 | /// Stroke color of the confetti (black by default, requires a strokeWidth > 0) 114 | final Color strokeColor; 115 | 116 | /// An optional parameter to set the minimum size potential size for 117 | /// the confetti. 118 | /// 119 | /// Must be smaller than the [maximumSize] attribute. 120 | final Size minimumSize; 121 | 122 | /// An optional parameter to set the maximum potential size for the confetti. 123 | /// Must be bigger than the [minimumSize] attribute. 124 | final Size maximumSize; 125 | 126 | /// An optional parameter to specify drag force, effecting the movement 127 | /// of the confetti. 128 | /// 129 | /// Using `1.0` will give no drag at all, while, for example, using `0.1` 130 | /// will give a lot of drag. Default is set to `0.05`. 131 | final double particleDrag; 132 | 133 | /// An optional parameter to specify the area size where the confetti will 134 | /// be thrown. 135 | /// 136 | /// By default this is set to the window size. 137 | final Size? canvas; 138 | 139 | /// If `true` new particles will not be created if the FPS is lower 140 | /// than 60. Default is `true`, set to `false` to ensure particles are always 141 | /// created, regardless of frame rate. 142 | final bool pauseEmissionOnLowFrameRate; 143 | 144 | /// Child widget to display 145 | final Widget? child; 146 | 147 | @override 148 | _ConfettiWidgetState createState() => _ConfettiWidgetState(); 149 | } 150 | 151 | class _ConfettiWidgetState extends State 152 | with SingleTickerProviderStateMixin { 153 | final GlobalKey _particleSystemKey = GlobalKey(); 154 | 155 | late AnimationController _animController; 156 | late Animation _animation; 157 | late ParticleSystem _particleSystem; 158 | 159 | /// Keeps track of emition position on screen layout changes 160 | late Offset _emitterPosition; 161 | 162 | /// Keeps track of the screen size on layout changes. 163 | /// 164 | /// Controls the sizing restrictions for when confetti should be visible. 165 | Size _screenSize = const Size(0, 0); 166 | 167 | @override 168 | void initState() { 169 | super.initState(); 170 | widget.confettiController.addListener(_handleChange); 171 | 172 | _particleSystem = ParticleSystem( 173 | emissionFrequency: widget.emissionFrequency, 174 | numberOfParticles: widget.numberOfParticles, 175 | maxBlastForce: widget.maxBlastForce, 176 | minBlastForce: widget.minBlastForce, 177 | gravity: widget.gravity, 178 | blastDirection: widget.blastDirection, 179 | blastDirectionality: widget.blastDirectionality, 180 | colors: widget.colors, 181 | minimumSize: widget.minimumSize, 182 | maximumSize: widget.maximumSize, 183 | particleDrag: widget.particleDrag, 184 | createParticlePath: widget.createParticlePath, 185 | ); 186 | 187 | _particleSystem.addListener(_particleSystemListener); 188 | 189 | _initAnimation(); 190 | } 191 | 192 | void _initAnimation() { 193 | _animController = AnimationController( 194 | vsync: this, duration: widget.confettiController.duration); 195 | _animation = Tween(begin: 0, end: 1).animate(_animController); 196 | _animation 197 | ..addListener(_animationListener) 198 | ..addStatusListener(_animationStatusListener); 199 | 200 | WidgetsBinding.instance.addPostFrameCallback((_) { 201 | if (widget.confettiController.state == ConfettiControllerState.playing) { 202 | _startAnimation(); 203 | _startEmission(); 204 | } 205 | }); 206 | } 207 | 208 | void _handleChange() { 209 | if (widget.confettiController.state == ConfettiControllerState.playing) { 210 | _startAnimation(); 211 | _startEmission(); 212 | } else if (widget.confettiController.state == 213 | ConfettiControllerState.stopped) { 214 | _stopEmission(); 215 | } else if (widget.confettiController.state == 216 | ConfettiControllerState.stoppedAndCleared) { 217 | _stopEmission(clearAllParticles: true); 218 | } else if (widget.confettiController.state == 219 | ConfettiControllerState.disposed) { 220 | _stopEmission(clearAllParticles: true); 221 | } 222 | } 223 | 224 | late var lastTime = DateTime.now().millisecondsSinceEpoch; 225 | 226 | void _animationListener() { 227 | if (_particleSystem.particleSystemStatus == ParticleSystemStatus.finished) { 228 | _animController.stop(); 229 | return; 230 | } 231 | final currentTime = DateTime.now().millisecondsSinceEpoch; 232 | final deltaTime = (currentTime - lastTime) / 1000; 233 | 234 | lastTime = currentTime; 235 | 236 | if (deltaTime > kLowLimit) { 237 | _particleSystem.update(kLowLimit, 238 | pauseEmission: widget.pauseEmissionOnLowFrameRate); 239 | } else { 240 | _particleSystem.update(deltaTime); 241 | } 242 | 243 | widget.confettiController.particleStatsCallback?.call( 244 | ParticleStats( 245 | numberOfParticles: _particleSystem.numberOfParticles, 246 | activeNumberOfParticles: _particleSystem.activeNumberOfParticles, 247 | ), 248 | ); 249 | } 250 | 251 | void _animationStatusListener(AnimationStatus status) { 252 | if (status == AnimationStatus.completed) { 253 | if (!widget.shouldLoop) { 254 | _stopEmission(); 255 | } 256 | _continueAnimation(); 257 | } 258 | } 259 | 260 | void _particleSystemListener() { 261 | if (_particleSystem.particleSystemStatus == ParticleSystemStatus.finished) { 262 | _stopAnimation(); 263 | } 264 | } 265 | 266 | void _startEmission() { 267 | _particleSystem.startParticleEmission(); 268 | } 269 | 270 | void _stopEmission({bool clearAllParticles = false}) { 271 | if (_particleSystem.particleSystemStatus == ParticleSystemStatus.stopped) { 272 | return; 273 | } 274 | _particleSystem.stopParticleEmission(clearAllParticles: clearAllParticles); 275 | } 276 | 277 | void _startAnimation() { 278 | // Make sure widgets are built before setting screen size and position 279 | if (mounted) { 280 | _setScreenSize(); 281 | _setEmitterPosition(); 282 | _animController.forward(from: 0); 283 | } 284 | } 285 | 286 | void _stopAnimation() { 287 | _animController.stop(); 288 | widget.confettiController.stop(); 289 | } 290 | 291 | void _continueAnimation() { 292 | _animController.forward(from: 0); 293 | } 294 | 295 | void _setScreenSize() { 296 | _screenSize = _getScreenSize(); 297 | _particleSystem.screenSize = _screenSize; 298 | } 299 | 300 | void _setEmitterPosition() { 301 | _emitterPosition = _getContainerPosition(); 302 | _particleSystem.particleSystemPosition = _emitterPosition; 303 | } 304 | 305 | Offset _getContainerPosition() { 306 | if (mounted) { 307 | final containerRenderBox = 308 | _particleSystemKey.currentContext?.findRenderObject() as RenderBox?; 309 | return containerRenderBox?.localToGlobal(Offset.zero) ?? Offset.zero; 310 | } else { 311 | return Offset.zero; 312 | } 313 | } 314 | 315 | Size _getScreenSize() { 316 | if (mounted) { 317 | try { 318 | return widget.canvas ?? MediaQuery.of(context).size; 319 | } catch (e) { 320 | // Ugh flutter debug on web throws an error here. Need to clean this 321 | // whole thing up. 322 | print('[flutter_confetti] Error getting screen size: $e'); 323 | return widget.canvas ?? Size.zero; 324 | } 325 | } else { 326 | return widget.canvas ?? Size.zero; 327 | } 328 | } 329 | 330 | /// On layout change update the position of the emitter 331 | /// and the screen size. 332 | /// 333 | /// Only update the emitter if it has already been set, to avoid RenderObject 334 | /// issues. 335 | /// 336 | /// The emitter position is first set in the `addPostFrameCallback` 337 | /// in [initState]. 338 | void _updatePositionAndSize() { 339 | // TODO: improve this 340 | if (_getScreenSize() != _screenSize) { 341 | _setScreenSize(); 342 | _setEmitterPosition(); 343 | } 344 | } 345 | 346 | @override 347 | Widget build(BuildContext context) { 348 | return LayoutBuilder( 349 | builder: (BuildContext context, BoxConstraints constraints) { 350 | // Update position and size only when layout changes 351 | WidgetsBinding.instance.addPostFrameCallback((_) { 352 | _updatePositionAndSize(); 353 | }); 354 | 355 | return RepaintBoundary( 356 | child: CustomPaint( 357 | key: _particleSystemKey, 358 | willChange: true, 359 | foregroundPainter: ParticlePainter( 360 | _animController, 361 | strokeWidth: widget.strokeWidth, 362 | strokeColor: widget.strokeColor, 363 | particles: _particleSystem.particles, 364 | paintEmitterTarget: widget.displayTarget, 365 | ), 366 | child: widget.child, 367 | ), 368 | ); 369 | }, 370 | ); 371 | } 372 | 373 | @override 374 | void dispose() { 375 | widget.confettiController.stop(); 376 | _animController.dispose(); 377 | widget.confettiController.removeListener(_handleChange); 378 | _particleSystem.removeListener(_particleSystemListener); 379 | super.dispose(); 380 | } 381 | } 382 | 383 | class ParticlePainter extends CustomPainter { 384 | ParticlePainter( 385 | Listenable? repaint, { 386 | required this.particles, 387 | bool paintEmitterTarget = true, 388 | Color emitterTargetColor = Colors.black, 389 | Color strokeColor = Colors.black, 390 | this.strokeWidth = 0, 391 | }) : _paintEmitterTarget = paintEmitterTarget, 392 | _emitterPaint = Paint() 393 | ..color = emitterTargetColor 394 | ..style = PaintingStyle.stroke 395 | ..strokeWidth = 2.0, 396 | _particlePaint = Paint() 397 | ..color = Colors.green 398 | ..style = PaintingStyle.fill, 399 | _particleStrokePaint = Paint() 400 | ..color = strokeColor 401 | ..strokeWidth = strokeWidth 402 | ..style = PaintingStyle.stroke, 403 | super(repaint: repaint); 404 | 405 | final List particles; 406 | 407 | final Paint _emitterPaint; 408 | final bool _paintEmitterTarget; 409 | final Paint _particlePaint; 410 | final Paint _particleStrokePaint; 411 | final double strokeWidth; 412 | 413 | @override 414 | void paint(Canvas canvas, Size size) { 415 | if (_paintEmitterTarget) { 416 | _paintEmitter(canvas); 417 | } 418 | _paintParticles(canvas); 419 | } 420 | 421 | // TODO: seperate this 422 | void _paintEmitter(Canvas canvas) { 423 | const radius = 10.0; 424 | canvas.drawCircle(Offset.zero, radius, _emitterPaint); 425 | final path = Path() 426 | ..moveTo(0, -radius) 427 | ..lineTo(0, radius) 428 | ..moveTo(-radius, 0) 429 | ..lineTo(radius, 0); 430 | canvas.drawPath(path, _emitterPaint); 431 | } 432 | 433 | void _paintParticles(Canvas canvas) { 434 | for (final particle in particles) { 435 | if (!particle.active) continue; 436 | 437 | // OG way to do it. 438 | // final rotationMatrix4 = Matrix4.identity() 439 | // ..setEntry(3, 2, 0.001) // perspective 440 | // ..translate(particle.location.dx, particle.location.dy) 441 | // ..rotateX(particle.angleX) 442 | // ..rotateY(particle.angleY) 443 | // ..rotateZ(particle.rotateZ ? particle.angleZ : 0.0); 444 | 445 | // Manual way to do it. Should be more performant, and less memory usage. 446 | // Should be the same as above... Looking good to me. 447 | final dx = particle.location.dx; 448 | final dy = particle.location.dy; 449 | final cosX = cos(particle.angleX); 450 | final sinX = sin(particle.angleX); 451 | final cosY = cos(particle.angleY); 452 | final sinY = sin(particle.angleY); 453 | final cosZ = particle.rotateZ ? cos(particle.angleZ) : 1.0; 454 | final sinZ = particle.rotateZ ? sin(particle.angleZ) : 0.0; 455 | 456 | final rotationMatrix4 = Matrix4( 457 | cosY * cosZ, 458 | cosX * sinZ + sinX * sinY * cosZ, 459 | sinX * sinZ - cosX * sinY * cosZ, 460 | 0.0, 461 | -cosY * sinZ, 462 | cosX * cosZ - sinX * sinY * sinZ, 463 | sinX * cosZ + cosX * sinY * sinZ, 464 | 0.0, 465 | sinY, 466 | -sinX * cosY, 467 | cosX * cosY, 468 | 0.001, // perspective 469 | dx, 470 | dy, 471 | 0.0, 472 | 1.0, 473 | ); 474 | 475 | final finalPath = particle.path.transform(rotationMatrix4.storage); 476 | canvas.drawPath(finalPath, _particlePaint..color = particle.color); 477 | if (strokeWidth > 0) { 478 | canvas.drawPath(finalPath, _particleStrokePaint); 479 | } 480 | } 481 | } 482 | 483 | @override 484 | bool shouldRepaint(CustomPainter oldDelegate) { 485 | return true; 486 | } 487 | } 488 | 489 | /// {@template particle_stats_callback} 490 | /// This callback provides [ParticleStats] as an argument. 491 | /// {@endtemplate} 492 | typedef ParticleStatsCallback = void Function(ParticleStats stats); 493 | 494 | class ConfettiController extends ChangeNotifier { 495 | ConfettiController({ 496 | this.duration = const Duration(seconds: 30), 497 | this.particleStatsCallback, 498 | }) : assert(!duration.isNegative && duration.inMicroseconds > 0); 499 | 500 | Duration duration; 501 | 502 | ConfettiControllerState _state = ConfettiControllerState.stopped; 503 | 504 | /// {@macro confetti_controller_state} 505 | ConfettiControllerState get state => _state; 506 | 507 | /// {@macro particle_stats_callback} 508 | final ParticleStatsCallback? particleStatsCallback; 509 | 510 | void play() { 511 | _state = ConfettiControllerState.playing; 512 | notifyListeners(); 513 | } 514 | 515 | void stop({bool clearAllParticles = false}) { 516 | // if state is already disposed, it can not be stopped. 517 | if (_state == ConfettiControllerState.disposed) return; 518 | 519 | if (clearAllParticles) { 520 | _state = ConfettiControllerState.stoppedAndCleared; 521 | } else { 522 | _state = ConfettiControllerState.stopped; 523 | } 524 | notifyListeners(); 525 | } 526 | 527 | @override 528 | void dispose() { 529 | _state = ConfettiControllerState.disposed; 530 | notifyListeners(); 531 | super.dispose(); 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | const double kLowLimit = 1 / 60; 2 | const desiredSpeed = 1 / kLowLimit; 3 | -------------------------------------------------------------------------------- /lib/src/enums/blast_directionality.dart: -------------------------------------------------------------------------------- 1 | /// {@template blast_directionality} 2 | /// Specifies the directionality of the blast for the particles. 3 | /// 4 | /// This enum has two possible values: 5 | /// - `directional`: the blast has a specific direction that must be provided. 6 | /// - `explosive`: the blast has no particular direction and will blast in all 7 | /// directions. 8 | /// {@endtemplate} 9 | enum BlastDirectionality { 10 | directional, 11 | explosive, 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/enums/confetti_controller_state.dart: -------------------------------------------------------------------------------- 1 | /// {@template confetti_controller_state} 2 | /// Represents the current state of the Confetti animation. 3 | /// 4 | /// This enum has two possible values: 5 | /// - `playing`: the Confetti animation is currently playing. 6 | /// - `stopped`: the Confetti animation is currently stopped. 7 | /// - `stoppedAndCleared`: the Confetti animation is currently stopped and all 8 | /// particles are immediately cleared. 9 | /// - `disposed`: the Confetti animation has been disposed. 10 | /// {@endtemplate} 11 | enum ConfettiControllerState { 12 | playing, 13 | stopped, 14 | stoppedAndCleared, 15 | disposed, 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' show Random; 2 | import 'dart:ui' show lerpDouble; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | final _rand = Random(); 7 | 8 | abstract class Helper { 9 | static double randomize(double min, double max) => 10 | lerpDouble(min, max, _rand.nextDouble())!; 11 | 12 | static Color randomColor() => 13 | Colors.primaries[_rand.nextInt(Colors.primaries.length)]; 14 | 15 | static bool randomBool() => _rand.nextBool(); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/particle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:confetti/src/constants.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:vector_math/vector_math.dart' as vmath; 8 | 9 | import 'package:confetti/src/helper.dart'; 10 | 11 | import 'enums/blast_directionality.dart'; 12 | 13 | /// {@template particle_system_status} 14 | /// Represents the current status of the particle system. 15 | /// 16 | /// This enum has three possible values: 17 | /// - `started`: the particle system has been started and is currently running. 18 | /// - `finished`: the particle system has finished running and is no longer active. 19 | /// - `stopped`: the particle system has been manually stopped and is no longer active. 20 | /// {@endtemplate} 21 | enum ParticleSystemStatus { 22 | started, 23 | finished, 24 | stopped, 25 | } 26 | 27 | class ParticleSystem extends ChangeNotifier { 28 | ParticleSystem({ 29 | required double emissionFrequency, 30 | required int numberOfParticles, 31 | required double maxBlastForce, 32 | required double minBlastForce, 33 | required double blastDirection, 34 | required BlastDirectionality blastDirectionality, 35 | required List? colors, 36 | required Size minimumSize, 37 | required Size maximumSize, 38 | required double particleDrag, 39 | required double gravity, 40 | Path Function(Size size)? createParticlePath, 41 | }) : assert(maxBlastForce > 0 && 42 | minBlastForce > 0 && 43 | emissionFrequency >= 0 && 44 | emissionFrequency <= 1 && 45 | numberOfParticles > 0 && 46 | minimumSize.width > 0 && 47 | minimumSize.height > 0 && 48 | maximumSize.width > 0 && 49 | maximumSize.height > 0 && 50 | minimumSize.width <= maximumSize.width && 51 | minimumSize.height <= maximumSize.height && 52 | particleDrag >= 0.0 && 53 | particleDrag <= 1 && 54 | minimumSize.height <= maximumSize.height), 55 | assert(gravity >= 0 && gravity <= 1), 56 | _blastDirection = blastDirection, 57 | _blastDirectionality = blastDirectionality, 58 | _gravity = gravity, 59 | _maxBlastForce = maxBlastForce, 60 | _minBlastForce = minBlastForce, 61 | _frequency = emissionFrequency, 62 | _numberOfParticles = numberOfParticles, 63 | _colors = colors, 64 | _minimumSize = minimumSize, 65 | _maximumSize = maximumSize, 66 | _particleDrag = particleDrag, 67 | _rand = Random(), 68 | _createParticlePath = createParticlePath; 69 | 70 | ParticleSystemStatus? _particleSystemStatus; 71 | 72 | final List _particles = []; 73 | 74 | /// A frequency between 0 and 1 to determine how often the emitter 75 | /// should emit new particles. 76 | final double _frequency; 77 | final int _numberOfParticles; 78 | final double _maxBlastForce; 79 | final double _minBlastForce; 80 | final double _blastDirection; 81 | final BlastDirectionality _blastDirectionality; 82 | final double _gravity; 83 | final List? _colors; 84 | final Size _minimumSize; 85 | final Size _maximumSize; 86 | final double _particleDrag; 87 | final Path Function(Size size)? _createParticlePath; 88 | 89 | Offset _particleSystemPosition = Offset.zero; 90 | Size _screenSize = Size.zero; 91 | 92 | late double _bottomBorder; 93 | late double _rightBorder; 94 | late double _leftBorder; 95 | 96 | final Random _rand; 97 | 98 | set particleSystemPosition(Offset position) { 99 | _particleSystemPosition = position; 100 | } 101 | 102 | set screenSize(Size size) { 103 | _screenSize = size; 104 | // needs to be called here to only set the borders once 105 | _setScreenBorderPositions(); 106 | } 107 | 108 | void stopParticleEmission({bool clearAllParticles = false}) { 109 | _particleSystemStatus = ParticleSystemStatus.stopped; 110 | if (clearAllParticles) { 111 | _particles.clear(); 112 | } 113 | } 114 | 115 | void startParticleEmission() { 116 | _particleSystemStatus = ParticleSystemStatus.started; 117 | } 118 | 119 | void finishParticleEmission() { 120 | _particles.clear(); 121 | _particleSystemStatus = ParticleSystemStatus.finished; 122 | } 123 | 124 | /// List of all [Particle]s. 125 | List get particles => _particles; 126 | 127 | /// The number of particles in memory. Consists of active and deactive 128 | /// particles. 129 | /// 130 | /// Old particles that are no longer visible are deactivated, and reused when 131 | /// needed. New particles are only created when there is an insuffient number 132 | /// of deactive particles in memory. 133 | int get numberOfParticles => _particles.length; 134 | 135 | /// The number of active particles currently animating and visible. 136 | /// 137 | /// This is not the same as [numberOfParticles]. 138 | int get activeNumberOfParticles => _particles.fold( 139 | 0, 140 | (previousValue, element) { 141 | if (element.active) { 142 | return previousValue + 1; 143 | } else { 144 | return previousValue; 145 | } 146 | }, 147 | ); 148 | 149 | /// {@macro particle_system_status} 150 | ParticleSystemStatus? get particleSystemStatus => _particleSystemStatus; 151 | 152 | /// Update the particle system animation by moving it forward. 153 | void update(double deltaTime, {bool pauseEmission = false}) { 154 | if (_particleSystemStatus != ParticleSystemStatus.finished) { 155 | _updateParticles(deltaTime); 156 | } 157 | 158 | if ((_particleSystemStatus == ParticleSystemStatus.stopped) && 159 | _particles.isEmpty) { 160 | finishParticleEmission(); 161 | notifyListeners(); 162 | } 163 | 164 | // Return early if pauseEmission is true 165 | if (pauseEmission) return; 166 | 167 | if (_particleSystemStatus == ParticleSystemStatus.started) { 168 | // If there are no particles then immediately generate particles 169 | // This also ensures that particles are emitted on the first frame 170 | if (particles.isEmpty) { 171 | _addParticles(_particles, number: _numberOfParticles); 172 | return; 173 | } 174 | 175 | // Determines whether to generate new particles based on the [frequency] 176 | final chanceToGenerate = _rand.nextDouble(); 177 | if (chanceToGenerate < _frequency) { 178 | _addParticles(_particles, number: _numberOfParticles); 179 | } 180 | } 181 | } 182 | 183 | void _setScreenBorderPositions() { 184 | _bottomBorder = _screenSize.height * 1.1; 185 | _rightBorder = _screenSize.width * 1.1; 186 | _leftBorder = _screenSize.width - _rightBorder; 187 | } 188 | 189 | void _updateParticles(double deltaTime) { 190 | // remove particles from memory if system is stopped, update and return 191 | if (_particleSystemStatus == ParticleSystemStatus.stopped) { 192 | _particles 193 | .removeWhere((particle) => _isOutsideOfBorder(particle.location)); 194 | for (final particle in _particles) { 195 | particle.update(deltaTime); 196 | } 197 | return; 198 | } 199 | 200 | // deactivate particles no longer visible and update rest 201 | for (final particle in _particles) { 202 | if (_isOutsideOfBorder(particle.location)) { 203 | particle.deactivate(); 204 | continue; 205 | } 206 | particle.update(deltaTime); 207 | } 208 | } 209 | 210 | bool _isOutsideOfBorder(Offset particleLocation) { 211 | final globalParticlePosition = particleLocation + _particleSystemPosition; 212 | return (globalParticlePosition.dy >= _bottomBorder) || 213 | (globalParticlePosition.dx >= _rightBorder) || 214 | (globalParticlePosition.dx <= _leftBorder); 215 | } 216 | 217 | void _addParticles(List particles, {int number = 1}) { 218 | int count = 0; 219 | 220 | for (final particle in particles) { 221 | if (!particle.active) { 222 | particle.reactivate(); 223 | count++; 224 | if (count == number) { 225 | return; // exit early, no need to generate more particles 226 | } 227 | } 228 | } 229 | 230 | // create more particles not enough in memory 231 | for (var i = 0; i < number - count; i++) { 232 | particles.add( 233 | Particle( 234 | _randomColor(), 235 | _randomSize(), 236 | _gravity, 237 | _particleDrag, 238 | _createParticlePath, 239 | generateParticleForceCallback: _generateParticleForce, 240 | ), 241 | ); 242 | } 243 | } 244 | 245 | double get _randomBlastDirection => 246 | vmath.radians(Random().nextInt(359).toDouble()); 247 | 248 | vmath.Vector2 _generateParticleForce() { 249 | var blastDirection = _blastDirection; 250 | if (_blastDirectionality == BlastDirectionality.explosive) { 251 | blastDirection = _randomBlastDirection; 252 | } 253 | final blastRadius = Helper.randomize(_minBlastForce, _maxBlastForce); 254 | final y = blastRadius * sin(blastDirection); 255 | final x = blastRadius * cos(blastDirection); 256 | return vmath.Vector2(x, y); 257 | } 258 | 259 | Color _randomColor() { 260 | if (_colors != null) { 261 | if (_colors!.length == 1) { 262 | return _colors![0]; 263 | } 264 | final index = _rand.nextInt(_colors!.length); 265 | return _colors![index]; 266 | } 267 | return Helper.randomColor(); 268 | } 269 | 270 | Size _randomSize() { 271 | return Size( 272 | Helper.randomize(_minimumSize.width, _maximumSize.width), 273 | Helper.randomize(_minimumSize.height, _maximumSize.height), 274 | ); 275 | } 276 | } 277 | 278 | typedef GenerateParticleForceCallback = vmath.Vector2 Function(); 279 | 280 | class Particle { 281 | Particle( 282 | Color color, 283 | Size size, 284 | this.gravity, 285 | double particleDrag, 286 | Path Function(Size size)? createParticlePath, { 287 | required this.generateParticleForceCallback, 288 | }) : _startUpForce = generateParticleForceCallback(), 289 | _color = color, 290 | _mass = Helper.randomize(1, 11), 291 | _particleDrag = particleDrag, 292 | _location = vmath.Vector2.zero(), 293 | _acceleration = vmath.Vector2.zero(), 294 | _velocity = 295 | vmath.Vector2(Helper.randomize(-3, 3), Helper.randomize(-3, 3)), 296 | _pathShape = createParticlePath != null 297 | ? createParticlePath(size) 298 | : createPath(size), 299 | _aVelocityX = Helper.randomize(-0.1, 0.1), 300 | _aVelocityY = Helper.randomize(-0.1, 0.1), 301 | _aVelocityZ = Helper.randomize(-0.1, 0.1), 302 | _rotateZ = Helper.randomBool(), 303 | gravityVector = vmath.Vector2( 304 | 0, 305 | lerpDouble(0.1, 5, gravity)!, 306 | ), 307 | _active = true; 308 | 309 | final double gravity; 310 | 311 | final vmath.Vector2 _startUpForce; 312 | final GenerateParticleForceCallback generateParticleForceCallback; 313 | 314 | final vmath.Vector2 _location; 315 | final vmath.Vector2 _velocity; 316 | final vmath.Vector2 _acceleration; 317 | 318 | final double _particleDrag; 319 | double _aX = 0; 320 | double _aVelocityX; 321 | double _aY = 0; 322 | double _aVelocityY; 323 | double _aZ = 0; 324 | double _aVelocityZ; 325 | final vmath.Vector2 gravityVector; 326 | late final _aAcceleration = 0.0001 / _mass; 327 | 328 | final Color _color; 329 | final double _mass; 330 | final Path _pathShape; 331 | 332 | bool _active; 333 | bool get active => _active; 334 | 335 | final bool _rotateZ; 336 | 337 | double _timeAlive = 0; 338 | vmath.Vector2 windforceUp = vmath.Vector2(0, -1); 339 | 340 | static Path createPath(Size size) { 341 | final pathShape = Path() 342 | ..moveTo(0, 0) 343 | ..lineTo(-size.width, 0) 344 | ..lineTo(-size.width, size.height) 345 | ..lineTo(0, size.height) 346 | ..close(); 347 | 348 | // TODO: remove when this is fixed: https://github.com/funwithflutter/flutter_confetti/issues/66 349 | if (kIsWeb) { 350 | pathShape 351 | ..lineTo(-size.width, 0) 352 | ..lineTo(-size.width, size.height) 353 | ..lineTo(0, size.height) 354 | ..close(); 355 | } 356 | 357 | return pathShape; 358 | } 359 | 360 | void reactivate() { 361 | _timeAlive = 0; 362 | 363 | final f = generateParticleForceCallback(); 364 | _startUpForce.setValues(f.x, f.y); 365 | 366 | _location.setValues(0, 0); 367 | _acceleration.setValues(0, 0); 368 | _velocity.setValues(Helper.randomize(-3, 3), Helper.randomize(-3, 3)); 369 | 370 | _aX = 0; 371 | _aY = 0; 372 | _aZ = 0; 373 | _aVelocityX = Helper.randomize(-0.1, 0.1); 374 | _aVelocityY = Helper.randomize(-0.1, 0.1); 375 | _aVelocityZ = Helper.randomize(-0.1, 0.1); 376 | 377 | gravityVector.setValues( 378 | 0, 379 | lerpDouble(0.1, 5, gravity)!, 380 | ); 381 | 382 | _active = true; 383 | } 384 | 385 | void deactivate() { 386 | _active = false; 387 | } 388 | 389 | void applyForce(vmath.Vector2 force, double deltaTimeSpeed) { 390 | final f = force.clone()..divide(vmath.Vector2.all(_mass)); 391 | _acceleration.add(f * deltaTimeSpeed); 392 | } 393 | 394 | void drag(double deltaTimeSpeed) { 395 | final speed = sqrt(pow(_velocity.x, 2) + pow(_velocity.y, 2)); 396 | final dragMagnitude = _particleDrag * speed * speed; 397 | final drag = _velocity.clone() 398 | ..multiply(vmath.Vector2.all(-1)) 399 | ..normalize() 400 | ..multiply(vmath.Vector2.all(dragMagnitude)); 401 | applyForce(drag, deltaTimeSpeed); 402 | } 403 | 404 | void update(double deltaTime) { 405 | final deltaTimeSpeed = deltaTime * desiredSpeed; 406 | drag(deltaTimeSpeed); 407 | 408 | if (_timeAlive < 5) { 409 | applyForce(_startUpForce, deltaTimeSpeed); 410 | } 411 | if (_timeAlive < 25) { 412 | applyForce(windforceUp, deltaTimeSpeed); 413 | _timeAlive += 1; 414 | } 415 | 416 | applyForce(gravityVector, deltaTimeSpeed); 417 | 418 | _velocity.add(_acceleration * deltaTimeSpeed); 419 | _location.add(_velocity * deltaTimeSpeed); 420 | _acceleration.setZero(); 421 | 422 | _aVelocityX += _aAcceleration; 423 | _aX += _aVelocityX * deltaTimeSpeed; 424 | 425 | _aVelocityY += _aAcceleration; 426 | _aY += _aVelocityY * deltaTimeSpeed; 427 | 428 | if (_rotateZ) { 429 | _aZ += _aVelocityZ * deltaTimeSpeed; 430 | _aVelocityZ += _aAcceleration; 431 | } 432 | } 433 | 434 | Offset get location { 435 | if (_location.x.isNaN || _location.y.isNaN) { 436 | return const Offset(0, 0); 437 | } 438 | return Offset(_location.x, _location.y); 439 | } 440 | 441 | Color get color => _color; 442 | Path get path => _pathShape; 443 | 444 | double get angleX => _aX; 445 | double get angleY => _aY; 446 | double get angleZ => _aZ; 447 | 448 | bool get rotateZ => _rotateZ; 449 | } 450 | -------------------------------------------------------------------------------- /lib/src/particle_stats.dart: -------------------------------------------------------------------------------- 1 | /// {@template particle_stats} 2 | /// Information about the particle system. 3 | /// {@endtemplate} 4 | class ParticleStats { 5 | /// {@macro particle_stats} 6 | const ParticleStats({ 7 | required this.numberOfParticles, 8 | required this.activeNumberOfParticles, 9 | }); 10 | 11 | /// The number of particles in memory. These will be cleared when the 12 | /// controller is destroyed or the animation is finished. 13 | final int numberOfParticles; 14 | 15 | /// The number of particles currently active and visible on screen. 16 | final int activeNumberOfParticles; 17 | 18 | /// Returns an empty [ParticleStats] with all values set to 0. 19 | factory ParticleStats.empty() => 20 | const ParticleStats(numberOfParticles: 0, activeNumberOfParticles: 0); 21 | 22 | @override 23 | String toString() => 24 | 'ParticleStats(numberOfParticles: $numberOfParticles, activeParticles: $activeNumberOfParticles)'; 25 | } 26 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_confetti 2 | 3 | packages: 4 | - /** 5 | 6 | scripts: 7 | lint:all: 8 | run: melos run analyze && melos run format 9 | description: Run all static analysis checks 10 | 11 | analyze: 12 | run: | 13 | melos exec -c 5 -- \ 14 | dart analyze . --fatal-infos 15 | description: | 16 | Run `dart analyze` in all packages. 17 | - Note: you can also rely on your IDEs Dart Analysis / Issues window. 18 | 19 | format: 20 | run: dart pub global run flutter_plugin_tools format 21 | description: | 22 | Build a specific example app for Android. 23 | - Requires `flutter_plugin_tools` (`pub global activate flutter_plugin_tools`). 24 | - Requires `clang-format` (can be installed via Brew on macOS). -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | fake_async: 40 | dependency: transitive 41 | description: 42 | name: fake_async 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.1" 46 | flutter: 47 | dependency: "direct main" 48 | description: flutter 49 | source: sdk 50 | version: "0.0.0" 51 | flutter_lints: 52 | dependency: "direct dev" 53 | description: 54 | name: flutter_lints 55 | url: "https://pub.dartlang.org" 56 | source: hosted 57 | version: "1.0.4" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | lints: 64 | dependency: transitive 65 | description: 66 | name: lints 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "1.0.1" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.12" 77 | material_color_utilities: 78 | dependency: transitive 79 | description: 80 | name: material_color_utilities 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.1.5" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.2" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.9.0" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.1" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.1" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.4.12" 145 | vector_math: 146 | dependency: "direct main" 147 | description: 148 | name: vector_math 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.1.2" 152 | sdks: 153 | dart: ">=2.17.0 <3.0.0" 154 | flutter: ">=3.0.0" 155 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: confetti 2 | description: Blast colorful confetti all over the screen. Celebrate in app 3 | achievements with style. Control the velocity, angle, gravity and amount of 4 | confetti. 5 | version: 0.7.0 6 | homepage: https://github.com/funwithflutter/flutter_confetti 7 | 8 | environment: 9 | sdk: ">=2.17.0 <3.0.0" 10 | flutter: ">=3.0.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | vector_math: ^2.1.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^1.0.4 21 | -------------------------------------------------------------------------------- /test/confetti_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:confetti/confetti.dart'; 4 | 5 | void main() { 6 | group('ConfettiController', () { 7 | test('throws assertion error when `duration` is not positive', () { 8 | expect(() => ConfettiController(duration: const Duration(days: -20)), 9 | throwsAssertionError); 10 | 11 | expect(() => ConfettiController(duration: const Duration(seconds: 0)), 12 | throwsAssertionError); 13 | 14 | expect( 15 | () => ConfettiController(duration: const Duration(milliseconds: 0)), 16 | throwsAssertionError); 17 | 18 | expect( 19 | () => ConfettiController(duration: const Duration(microseconds: 0)), 20 | throwsAssertionError); 21 | }); 22 | }); 23 | } 24 | --------------------------------------------------------------------------------