├── .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 |
--------------------------------------------------------------------------------