├── .DS_Store
├── .github
└── workflows
│ ├── build-web-editor.yml
│ └── publish.yml
├── .gitignore
├── .pubignore
├── .vscode
├── launch.json
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── editor
├── .github
│ └── workflows
│ │ └── build-web.yml
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── assets
│ ├── app_configs.json
│ ├── favicon.png
│ └── texture.png
├── lib
│ ├── data
│ │ ├── inspector.dart
│ │ ├── particular_editor_config.dart
│ │ ├── particular_editor_controller.dart
│ │ └── particular_editor_layer.dart
│ ├── display
│ │ ├── footer_view.dart
│ │ ├── header_view.dart
│ │ ├── inspector_view.dart
│ │ ├── timeline_view.dart
│ │ └── widgets
│ │ │ └── footer_icon_button.dart
│ ├── main.dart
│ ├── services
│ │ ├── downloader
│ │ │ ├── file_saver_io.dart
│ │ │ ├── file_saver_none.dart
│ │ │ └── file_saver_web.dart
│ │ └── io.dart
│ └── theme
│ │ └── theme.dart
├── pubspec.yaml
├── test
│ └── widget_test.dart
└── web
│ ├── favicon.png
│ ├── icons
│ ├── Icon-192.png
│ ├── Icon-512.png
│ ├── Icon-maskable-192.png
│ └── Icon-maskable-512.png
│ ├── index.html
│ └── manifest.json
├── example
├── .gitignore
├── .metadata
├── README.md
├── analysis_options.yaml
├── assets
│ ├── firework.json
│ ├── galaxy.json
│ ├── meteor.json
│ ├── snow.json
│ └── texture.png
├── integration_test
│ └── plugin_integration_test.dart
├── lib
│ └── main.dart
├── macos
│ ├── Flutter
│ │ ├── GeneratedPluginRegistrant.swift
│ │ └── ephemeral
│ │ │ ├── .app_filename
│ │ │ ├── Flutter-Generated.xcconfig
│ │ │ ├── FlutterInputs.xcfilelist
│ │ │ ├── FlutterMacOS.podspec
│ │ │ ├── FlutterOutputs.xcfilelist
│ │ │ ├── flutter_export_environment.sh
│ │ │ └── tripwire
│ ├── Podfile
│ └── Pods
│ │ ├── Local Podspecs
│ │ └── FlutterMacOS.podspec.json
│ │ ├── Pods.xcodeproj
│ │ ├── project.pbxproj
│ │ └── xcuserdata
│ │ │ └── mansourdjawadi.xcuserdatad
│ │ │ └── xcschemes
│ │ │ ├── FlutterMacOS.xcscheme
│ │ │ ├── Pods-Runner.xcscheme
│ │ │ ├── Pods-RunnerTests.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ └── Target Support Files
│ │ ├── FlutterMacOS
│ │ ├── FlutterMacOS.debug.xcconfig
│ │ └── FlutterMacOS.release.xcconfig
│ │ ├── Pods-Runner
│ │ ├── Pods-Runner-Info.plist
│ │ ├── Pods-Runner-acknowledgements.markdown
│ │ ├── Pods-Runner-acknowledgements.plist
│ │ ├── Pods-Runner-dummy.m
│ │ ├── Pods-Runner-umbrella.h
│ │ ├── Pods-Runner.debug.xcconfig
│ │ ├── Pods-Runner.modulemap
│ │ ├── Pods-Runner.profile.xcconfig
│ │ └── Pods-Runner.release.xcconfig
│ │ └── Pods-RunnerTests
│ │ ├── Pods-RunnerTests-Info.plist
│ │ ├── Pods-RunnerTests-acknowledgements.markdown
│ │ ├── Pods-RunnerTests-acknowledgements.plist
│ │ ├── Pods-RunnerTests-dummy.m
│ │ ├── Pods-RunnerTests-umbrella.h
│ │ ├── Pods-RunnerTests.debug.xcconfig
│ │ ├── Pods-RunnerTests.modulemap
│ │ ├── Pods-RunnerTests.profile.xcconfig
│ │ └── Pods-RunnerTests.release.xcconfig
├── pubspec.yaml
└── test
│ └── widget_test.dart
├── lib
├── particular.dart
└── src
│ ├── blending.dart
│ ├── image_loader.dart
│ ├── particle.dart
│ ├── particular_configs.dart
│ ├── particular_controller.dart
│ ├── particular_emitter.dart
│ └── particular_layer.dart
├── particular.iml
├── pubspec.yaml
├── repo_files
├── editor_left.gif
├── editor_right.png
├── example_firework.webp
├── example_galaxy.webp
├── example_meteor.webp
├── example_snow.webp
└── logo.png
└── test
└── particular_test.dart
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/.DS_Store
--------------------------------------------------------------------------------
/.github/workflows/build-web-editor.yml:
--------------------------------------------------------------------------------
1 | name: Build and distribute
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v[0-9]+.[0-9]+.[0-9]+*' # tag pattern on pub.dev: 'v{{version}'
7 |
8 | jobs:
9 | build:
10 | name: build
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: subosito/flutter-action@v2
15 | - uses: bluefireteam/flutter-gh-pages@v7
16 | with:
17 | workingDir: editor
18 | baseHref: /particular/
19 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # .github/workflows/publish.yml
2 | name: Publish to pub.dev
3 |
4 | on:
5 | push:
6 | tags:
7 | - 'v[0-9]+.[0-9]+.[0-9]+*' # tag pattern on pub.dev: 'v{{version}'
8 |
9 | # Publish using custom workflow
10 | jobs:
11 | publish:
12 | permissions:
13 | id-token: write # Required for authentication using OIDC
14 | # content: read
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | # Setup Flutter SDK and automated pub.dev credentials
20 | - uses: flutter-actions/setup-flutter@v3
21 | - uses: flutter-actions/setup-pubdev-credentials@v1
22 |
23 | - name: Install dependencies
24 | run: flutter pub get
25 |
26 | - name: Publish
27 | run: flutter pub publish --force
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://www.dartlang.org/guides/libraries/private-files
2 |
3 | # Files and directories created by pub
4 | .dart_tool/
5 | .packages
6 | build/
7 | # If you're building an application, you may want to check-in your pubspec.lock
8 | pubspec.lock
9 |
10 | # Directory created by dartdoc
11 | # If you don't generate documentation locally you can remove this line.
12 | doc/api/
13 |
14 | # dotenv environment variables file
15 | .env*
16 |
17 | # Avoid committing generated Javascript files:
18 | *.dart.js
19 | *.info.json # Produced by the --dump-info flag.
20 | *.js # When generated by dart2js. Don't specify *.js if your
21 | # project includes source files written in JavaScript.
22 | *.js_
23 | *.js.deps
24 | *.js.map
25 |
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 |
29 | .idea/
30 |
--------------------------------------------------------------------------------
/.pubignore:
--------------------------------------------------------------------------------
1 | /docs/*
2 | /editor/*
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "particular",
9 | "request": "launch",
10 | "type": "dart"
11 | },
12 | {
13 | "name": "particular (profile mode)",
14 | "request": "launch",
15 | "type": "dart",
16 | "flutterMode": "profile"
17 | },
18 | {
19 | "name": "particular (release mode)",
20 | "request": "launch",
21 | "type": "dart",
22 | "flutterMode": "release"
23 | },
24 | {
25 | "name": "editor",
26 | "cwd": "editor",
27 | "request": "launch",
28 | "type": "dart"
29 | },
30 | {
31 | "name": "editor (profile mode)",
32 | "cwd": "editor",
33 | "request": "launch",
34 | "type": "dart",
35 | "flutterMode": "profile"
36 | },
37 | {
38 | "name": "editor (release mode)",
39 | "cwd": "editor",
40 | "request": "launch",
41 | "type": "dart",
42 | "flutterMode": "release"
43 | },
44 | {
45 | "name": "example",
46 | "cwd": "example",
47 | "request": "launch",
48 | "type": "dart"
49 | },
50 | {
51 | "name": "example (profile mode)",
52 | "cwd": "example",
53 | "request": "launch",
54 | "type": "dart",
55 | "flutterMode": "profile"
56 | },
57 | {
58 | "name": "example (release mode)",
59 | "cwd": "example",
60 | "request": "launch",
61 | "type": "dart",
62 | "flutterMode": "release"
63 | }
64 | ]
65 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### 0.3.4 (02/10/2025)
2 | * feature: add emitter rotation property
3 | * enhance: wasm support
4 | * fixbugs: fix some minor bugs
5 |
6 | ### 0.3.3 (10/25/2024)
7 | * feature: import configs with texture files
8 | * feature: add custom layer with custom texture
9 | * enhance: dispose layer ability
10 |
11 | ### 0.3.2 (10/15/2024)
12 | * feature: new particle editor
13 | * enhance: add automate publish
14 | * fixbugs: wrong particles count of limit-time layers
15 | * fixbugs: wrong afew particles count
16 |
17 | ### 0.2.5 (06/12/2024)
18 | * add automate publish
19 |
20 | ### 0.2.2 (05/23/2024)
21 | * fixbugs: handle low number particles
22 |
23 | ### 0.2.1 (05/03/2024)
24 | * feature: multilayer particles support
25 | * enhance: use mutual emitters for increase performance
26 | * cleanups: better code instructions
27 |
28 | ### 0.2.0 (05/01/2024)
29 | * feature: support finite and infinite particles
30 | * feature: support multi-layer particle
31 | * feature: add timeline in editor
32 | * feature: new editor theme
33 | * enhance: better ux for inputs
34 |
35 | ### 0.1.0 (04/26/2024)
36 | * feature: add editor (initial version)
37 | * enhance: replace blending functions
38 | * enhance: limited time particle
39 | * fixbugs: resize image rects on texture changes
40 |
41 | ### 0.0.5 (04/19/2024)
42 | * fix instruction problem
43 | * add more symbol comments
44 |
45 | ### 0.0.4 (04/18/2024)
46 | * fix example image flicking
47 | * follow format suggestions
48 | * rich instruction
49 |
50 | ### 0.0.3 (04/18/2024)
51 | * add some method and class documentations
52 | * follow format suggestions
53 |
54 | ### 0.0.2 (04/17/2024)
55 | * add some method and class documentations
56 | * remove platforms section in pubs-pecs
57 | * remove platform classes and tests
58 | * add pub-ignores
59 |
60 | ### 0.0.1 (04/17/2024)
61 | * initial version
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mansour Djawadi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Enhance your app or game visuals with this high-performance Flutter Particles System widget. Utilize JSON or programmatic configuration, seamlessly integrating with popular particles editors for effortless customization.
5 |
6 |
7 | Customizable (live) Particle Effects.
8 | Ready Presets (JSON Configs).
9 | Seamless Integration with Editors.
10 | Optimized Performance with 1~10k particle at frame
11 |
12 | Whether you're a designer or developer, Particular empowers you to bring your creative visions with ease.
13 |
14 | ---
15 |
16 | ### - Some Presets:
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ---
28 |
29 | ### - Installation
30 | Add `particular` to your pubspec.yaml file:
31 | For detailed installation instructions, refer to the [installation guide](https://pub.dev/packages/particular/install) on pub.dev.
32 |
33 |
34 | ---
35 |
36 | ### - Configurate your particles
37 | You have two options for configuring your particles:
38 | 1. Using Editors:
39 |
40 | Generate your particles system configurations by [Particular Editor](https://manjav.github.io/particular).
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 2. Programmatic Configuration:
51 | Manually configure your particle controller in code. Refer to the following steps for more details.
52 |
53 | ---
54 |
55 | ### - Getting Started with Coding
56 | Follow these steps to integrate the particles system into your Flutter app:
57 |
58 | I. Insert Particle Files into Your Project:
59 | The [Particular Editor](https://manjav.github.io/particular) exports one or multiple particle layers. For each particle layer, it generates a configuration file (configs.json) and a related image file (texture.png). In the configuration file, there is a node named textureFileName, which refers to the image file. Place the image and configs.json file in your project's assets folder, and add the necessary assets entry in your pubspec.yaml file.
60 | ``` yml
61 | assets:
62 | - assets/configs.json
63 | - assets/texture.png
64 | ```
65 | https://docs.flutter.dev/ui/assets/assets-and-images
66 |
67 |
68 | II. Initialize the Particles Controller in `initState`:
69 |
70 | To use this library, import `package:particular/particular.dart`.
71 | ``` dart
72 | final _particleController = ParticularController();
73 | ...
74 | @override
75 | void initState() {
76 | _loadParticleAssets();
77 | super.initState();
78 | }
79 |
80 | // Load configs and texture of particle
81 | Future _loadParticleAssets() async {
82 |
83 | // Load particle configs file
84 | String json = await rootBundle.loadString("assets/configs.json");
85 | final configsData = jsonDecode(json);
86 |
87 | // Load particle texture file
88 | ByteData bytes = await rootBundle.load("assets/${configsData["textureFileName"]}");
89 | ui.Image texture = await loadUIImage(bytes.buffer.asUint8List());
90 |
91 | // Add particles layer
92 | _particleController.addLayer(
93 | texture: frameInfo.image, // Remove in default-texture case
94 | configsData: configsData, // Remove in programmatic configuration case
95 | );
96 | }
97 | ```
98 |
99 |
100 | III. Add the `Particular` widget in your widget three:
101 | ``` dart
102 | @override
103 | Widget build(BuildContext context) {
104 | return MaterialApp(
105 | home: Scaffold(
106 | backgroundColor: Colors.black,
107 | body: Particular(
108 | controller: _particleController,
109 | ),
110 | ),
111 | );
112 | }
113 | ```
114 |
115 |
116 | IIII. Live Update Particle Layer:
117 | ``` dart
118 | _particleController.layers.first.update(
119 | maxParticles: 100,
120 | lifespan:1.2,
121 | speed:100,
122 | angle:30,
123 | );
124 | ```
125 |
126 |
127 |
128 | You can also use different image types supported by Flutter, with varying names and locations, following the guidelines below:
129 | ``` json
130 | {
131 | "textureFileName": "images/particle_snow.webp"
132 | }
133 | ```
134 | ``` yml
135 | assets:
136 | - assets/data/particle_snow.json
137 | - assets/images/particle_snow.webp
138 | ```
139 | ``` dart
140 | ...
141 |
142 | // Load particle configs file
143 | String json = await rootBundle.loadString("assets/data/particle_snow.json");
144 |
145 | ...
146 | ```
147 | ---
148 |
149 | This revised README provides clear installation instructions, options for configuring particles, and steps for integrating and customizing the particle system in your Flutter app. If you have any questions or need further assistance, don't hesitate to ask!
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | # Additional information about this file can be found at
4 | # https://dart.dev/guides/language/analysis-options
5 |
--------------------------------------------------------------------------------
/editor/.github/workflows/build-web.yml:
--------------------------------------------------------------------------------
1 | name: Build and distribute
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'a[0-9]+.[0-9]+.[0-9]+*' # tag pattern on pub.dev: 'v{{version}'
7 |
8 | jobs:
9 | build:
10 | name: build
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: subosito/flutter-action@v2
15 | - uses: bluefireteam/flutter-gh-pages@v7
16 | with:
17 | workingDir: editor
--------------------------------------------------------------------------------
/editor/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
13 | migrate_working_dir/
14 |
15 | # IntelliJ related
16 | *.iml
17 | *.ipr
18 | *.iws
19 | .idea/
20 |
21 | # The .vscode folder contains launch configuration and tasks you configure in
22 | # VS Code which you may wish to be included in version control, so this line
23 | # is commented out by default.
24 | #.vscode/
25 |
26 | # Flutter/Dart/Pub related
27 | **/doc/api/
28 | **/ios/Flutter/.last_build_id
29 | .dart_tool/
30 | .flutter-plugins
31 | .flutter-plugins-dependencies
32 | .pub-cache/
33 | .pub/
34 | /build/
35 | /macos/
36 | /android/
37 |
38 | # Symbolication related
39 | app.*.symbols
40 |
41 | # Obfuscation related
42 | app.*.map.json
43 |
44 | # Android Studio will place build artifacts here
45 | /android/app/debug
46 | /android/app/profile
47 | /android/app/release
48 |
--------------------------------------------------------------------------------
/editor/.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: "2663184aa79047d0a33a14a3b607954f8fdd8730"
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: 54e66469a933b60ddf175f858f82eaeb97e48c8d
17 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
18 | - platform: web
19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/editor/README.md:
--------------------------------------------------------------------------------
1 | # editor
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://docs.flutter.dev/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
13 |
14 | For help getting started with Flutter development, view the
15 | [online documentation](https://docs.flutter.dev/), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/editor/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 https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/editor/assets/app_configs.json:
--------------------------------------------------------------------------------
1 | {
2 | "appBarHeight": 40.0,
3 | "footerHeight": 24.0,
4 | "timeline": {
5 | "height": 160.0,
6 | "layerHeight": 32.0,
7 | "sideWidth": 220.0
8 | },
9 | "inspector": {
10 | "width": 250.0,
11 | "components": [
12 | {
13 | "title": "Emission Settings",
14 | "children": [
15 | {
16 | "title": "Duration",
17 | "min": -1.0,
18 | "inputs": {
19 | "Start": "startTime",
20 | "End": "endTime"
21 | }
22 | },
23 | {
24 | "title": "Emitter Position",
25 | "inputs": {
26 | "X": "emitterX",
27 | "Y": "emitterY"
28 | }
29 | },
30 | {
31 | "title": "Emitter Shape Size",
32 | "inputs": {
33 | "Width": "sourcePositionVarianceX",
34 | "Height": "sourcePositionVarianceY"
35 | }
36 | },
37 | {
38 | "title": "Emitter Shape Rotation",
39 | "inputs": {
40 | "Angle°": "sourcePositionRotation"
41 | }
42 | },
43 | {
44 | "min": 0.0,
45 | "inputs": {
46 | "Quantitiy": "maxParticles"
47 | }
48 | },
49 | {
50 | "inputs": {
51 | "Angle": "angle",
52 | "+\n-": "angleVariance"
53 | }
54 | },
55 | {},
56 | {
57 | "ui": "dropdown",
58 | "inputs": {
59 | "Emission": "emitterType"
60 | }
61 | },
62 | {
63 | "type": "gravity",
64 | "inputs": {
65 | "Speed": "speed",
66 | "+\n-": "speedVariance"
67 | }
68 | },
69 | {
70 | "type": "gravity",
71 | "title": "Gravity",
72 | "inputs": {
73 | "x": "gravityX",
74 | "Y": "gravityY"
75 | }
76 | },
77 | {
78 | "type": "gravity",
79 | "inputs": {
80 | "Rad Force": "radialAcceleration",
81 | "+\n-": "radialAccelerationVariance"
82 | }
83 | },
84 | {
85 | "type": "gravity",
86 | "inputs": {
87 | "Tan Force": "tangentialAcceleration",
88 | "+\n-": "tangentialAccelerationVariance"
89 | }
90 | },
91 | {
92 | "type": "radial",
93 | "inputs": {
94 | "Min Radius": "minRadius",
95 | "+\n-": "minRadiusVariance"
96 | }
97 | },
98 | {
99 | "type": "radial",
100 | "inputs": {
101 | "Max Radius": "maxRadius",
102 | "+\n-": "maxRadiusVariance"
103 | }
104 | },
105 | {
106 | "type": "radial",
107 | "inputs": {
108 | "Rotation Per Second": "rotatePerSecond",
109 | "+\n-": "rotatePerSecondVariance"
110 | }
111 | }
112 | ]
113 | },
114 | {
115 | "title": "Particle Settings",
116 | "children": [
117 | {
118 | "inputs": {
119 | "Lifespan": "lifespan",
120 | "+\n-": "lifespanVariance"
121 | }
122 | },
123 | {
124 | "inputs": {
125 | "Start Size": "startSize",
126 | "+\n-": "startSizeVariance"
127 | }
128 | },
129 | {
130 | "inputs": {
131 | "End Size": "finishSize",
132 | "+\n-": "finishSizeVariance"
133 | }
134 | },
135 | {
136 | "inputs": {
137 | "Start Rotation": "startRotation",
138 | "+\n-": "startRotationVariance"
139 | }
140 | },
141 | {
142 | "inputs": {
143 | "End Rotation": "finishRotation",
144 | "+\n-": "finishRotationVariance"
145 | }
146 | },
147 | {
148 | "ui": "color",
149 | "inputs": {
150 | "Start Color": "startColor",
151 | "+\n-": "startColorVariance"
152 | }
153 | },
154 | {
155 | "ui": "color",
156 | "inputs": {
157 | "End Color": "finishColor",
158 | "+\n-": "finishColorVariance"
159 | }
160 | },
161 | {
162 | "ui": "dropdown",
163 | "inputs": {
164 | "Paint Blending": "textureBlendMode"
165 | }
166 | },
167 | {
168 | "ui": "dropdown",
169 | "inputs": {
170 | "Draw Blending": "renderBlendMode"
171 | }
172 | },
173 | {
174 | "ui": "button",
175 | "inputs": {
176 | "texture": "load"
177 | }
178 | }
179 | ]
180 | }
181 | ]
182 | }
183 | }
--------------------------------------------------------------------------------
/editor/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/assets/favicon.png
--------------------------------------------------------------------------------
/editor/assets/texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/assets/texture.png
--------------------------------------------------------------------------------
/editor/lib/data/inspector.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class InspectorList {
4 | final String title;
5 | final List children;
6 | InspectorList(this.title, this.children);
7 | }
8 |
9 | class Inspector {
10 | final String? ui;
11 | final String? type;
12 | final String title;
13 | final Map inputs;
14 | final double? min;
15 | final double? max;
16 |
17 | Inspector(
18 | this.ui,
19 | this.min,
20 | this.max,
21 | this.type,
22 | this.title,
23 | this.inputs,
24 | );
25 |
26 | static final ValueNotifier list =
27 | ValueNotifier(InspectorList("", []));
28 | }
29 |
--------------------------------------------------------------------------------
/editor/lib/data/particular_editor_config.dart:
--------------------------------------------------------------------------------
1 | import 'package:particular/particular.dart';
2 |
3 | /// Extension methods for `ParticularConfigs` class
4 | extension ParticularEditorConfig on ParticularConfigs {
5 | /// Gets the value of the given parameter.
6 | dynamic getParam(String key) {
7 | return switch (key) {
8 | "configName" => configName,
9 | "textureFileName" => textureFileName,
10 | "emitterType" => emitterType,
11 | "renderBlendMode" => renderBlendMode,
12 | "textureBlendMode" => textureBlendMode,
13 | "blendFunctionSource" => blendFunctionSource,
14 | "blendFunctionDestination" => blendFunctionDestination,
15 | "startTime" => startTime,
16 | "endTime" => endTime,
17 | "lifespan" => lifespan,
18 | "lifespanVariance" => lifespanVariance,
19 | "maxParticles" => maxParticles,
20 | "startColor" => startColor,
21 | "startColorVariance" => startColorVariance,
22 | "finishColor" => finishColor,
23 | "finishColorVariance" => finishColorVariance,
24 | "sourcePositionVarianceX" => sourcePositionVarianceX,
25 | "sourcePositionVarianceY" => sourcePositionVarianceY,
26 | "sourcePositionRotation" => sourcePositionRotation,
27 | "startSize" => startSize,
28 | "startSizeVariance" => startSizeVariance,
29 | "angle" => angle,
30 | "finishSize" => finishSize,
31 | "finishSizeVariance" => finishSizeVariance,
32 | "speed" => speed,
33 | "speedVariance" => speedVariance,
34 | "angleVariance" => angleVariance,
35 | "emitterX" => emitterX,
36 | "emitterY" => emitterY,
37 | "gravityX" => gravityX,
38 | "gravityY" => gravityY,
39 | "minRadius" => minRadius,
40 | "minRadiusVariance" => minRadiusVariance,
41 | "maxRadius" => maxRadius,
42 | "maxRadiusVariance" => maxRadiusVariance,
43 | "rotatePerSecond" => rotatePerSecond,
44 | "rotatePerSecondVariance" => rotatePerSecondVariance,
45 | "startRotation" => startRotation,
46 | "startRotationVariance" => startRotationVariance,
47 | "finishRotation" => finishRotation,
48 | "finishRotationVariance" => finishRotationVariance,
49 | "radialAcceleration" => radialAcceleration,
50 | "radialAccelerationVariance" => radialAccelerationVariance,
51 | "tangentialAcceleration" => tangentialAcceleration,
52 | "tangentialAccelerationVariance" => tangentialAccelerationVariance,
53 | _ => null,
54 | };
55 | }
56 |
57 | /// Convert to Map for export
58 | Map toMap() {
59 | return {
60 | "configName": configName,
61 | "textureFileName": textureFileName,
62 | "emitterType": emitterType.index,
63 | "renderBlendMode": renderBlendMode.index,
64 | "textureBlendMode": textureBlendMode.index,
65 | "particleLifespan": (lifespan * 0.001),
66 | "particleLifespanVariance": lifespanVariance * 0.001,
67 | "startTime": startTime * 0.001,
68 | "duration": endTime * (endTime > -1 ? 0.001 : 1),
69 | "maxParticles": maxParticles,
70 | "sourcePositionVariancex": sourcePositionVarianceX,
71 | "sourcePositionVariancey": sourcePositionVarianceY,
72 | "sourcePositionRotation": sourcePositionRotation,
73 | "startParticleSize": startSize,
74 | "startParticleSizeVariance": startSizeVariance,
75 | "finishParticleSize": finishSize,
76 | "finishParticleSizeVariance": finishSizeVariance,
77 | "speed": speed,
78 | "speedVariance": speedVariance,
79 | "emitterX": emitterX,
80 | "emitterY": emitterY,
81 | "gravityx": gravityX,
82 | "gravityy": gravityY,
83 | "minRadius": minRadius,
84 | "minRadiusVariance": minRadiusVariance,
85 | "maxRadius": maxRadius,
86 | "maxRadiusVariance": maxRadiusVariance,
87 | "angle": angle,
88 | "angleVariance": angleVariance,
89 | "rotatePerSecond": rotatePerSecond,
90 | "rotatePerSecondVariance": rotatePerSecondVariance,
91 | "rotationStart": startRotation,
92 | "rotationStartVariance": startRotationVariance,
93 | "rotationEnd": finishRotation,
94 | "rotationEndVariance": finishRotationVariance,
95 | "radialAcceleration": radialAcceleration,
96 | "radialAccelVariance": radialAccelerationVariance,
97 | "tangentialAcceleration": tangentialAcceleration,
98 | "tangentialAccelVariance": tangentialAccelerationVariance,
99 | ...startColor.toMap("startColor"),
100 | ...startColor.toMap("startColorVariance"),
101 | ...startColor.toMap("finishColor"),
102 | ...startColor.toMap("finishColorVariance"),
103 | };
104 | }
105 | }
106 |
107 | /// Extension methods for `ARGB` class
108 | extension ARGBExtension on ARGB {
109 | /// Convert to Map for export
110 | Map toMap(String name) {
111 | return {
112 | "${name}Alpha": a,
113 | "${name}Red": r,
114 | "${name}Green": g,
115 | "${name}Blue": b
116 | };
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/editor/lib/data/particular_editor_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui' as ui;
2 |
3 | import 'package:editor/data/particular_editor_layer.dart';
4 | import 'package:flutter/foundation.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:particular/particular.dart';
7 |
8 | /// Extension methods for `ParticularConfigs` class
9 | class ParticularEditorController extends ParticularController {
10 | /// The default texture bytes for the particle system.
11 | Uint8List? defaultTextureBytes;
12 |
13 | /// The default texture for the particle system.
14 | ui.Image? defaultTexture;
15 |
16 | /// The default texture for the particle system.
17 | Future getDefaultTexture() async {
18 | if (defaultTexture == null) {
19 | ByteData bytes = await rootBundle.load("assets/texture.png");
20 | defaultTextureBytes = bytes.buffer.asUint8List();
21 | defaultTexture = await loadUIImage(defaultTextureBytes!);
22 | }
23 | return defaultTexture!;
24 | }
25 |
26 | /// Adds a new particle system to the application.
27 | @override
28 | Future addConfigs({Map? configsData}) async {
29 | ByteData bytes;
30 |
31 | /// Load particle texture
32 | ui.Image? texture;
33 | try {
34 | if (configsData != null && configsData.containsKey("textureFileName")) {
35 | bytes =
36 | await rootBundle.load("assets/${configsData["textureFileName"]}");
37 | texture = await loadUIImage(bytes.buffer.asUint8List());
38 | }
39 | } catch (e) {
40 | debugPrint(e.toString());
41 | }
42 | final configs = ParticularConfigs.initialize(configs: configsData);
43 |
44 | final layer = ParticularEditorLayer(
45 | texture: texture ?? await getDefaultTexture(),
46 | textureBytes: defaultTextureBytes,
47 | configs: configs,
48 | );
49 |
50 | if (configsData == null || !configsData.containsKey("configName")) {
51 | configs.updateWith({"configName": "Layer ${layers.length + 1}"});
52 | }
53 | addParticularLayer(layer);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/editor/lib/data/particular_editor_layer.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:particular/particular.dart';
4 |
5 | /// The layer for the particle system.
6 | /// It contains the texture and the configs for the layer.
7 | class ParticularEditorLayer extends ParticularLayer {
8 | /// The bytes of the texture used for particles.
9 | Uint8List? textureBytes;
10 |
11 | /// Initializes a new instance of the `ParticularEditorLayer` class.
12 | ParticularEditorLayer({
13 | required super.texture,
14 | required this.textureBytes,
15 | required super.configs,
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/editor/lib/display/footer_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:editor/data/particular_editor_controller.dart';
2 | import 'package:editor/theme/theme.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:particular/particular.dart';
5 |
6 | import 'widgets/footer_icon_button.dart';
7 |
8 | /// The footer line for the application that contains the buttons for layers.
9 | class FooterView extends StatelessWidget {
10 | /// The configurations for the application.
11 | final Map appConfigs;
12 |
13 | /// The controller for the particle system.
14 | final ParticularEditorController controller;
15 |
16 | /// Creates a footer view.
17 | const FooterView({
18 | super.key,
19 | required this.appConfigs,
20 | required this.controller,
21 | });
22 |
23 | /// Creates a footer view.
24 | @override
25 | Widget build(BuildContext context) {
26 | return ListenableBuilder(
27 | listenable: controller.getNotifier(NotifierType.layer),
28 | builder: (context, child) {
29 | return Container(
30 | color: Colors.white10,
31 | height: appConfigs["footerHeight"],
32 | child: Row(
33 | children: [
34 | FooterIconButton(
35 | icon: Icons.refresh,
36 | onPressed: () => controller.resetTick(),
37 | tooltip: 'Reset time',
38 | ),
39 | // SizedBox(width: appConfigs["timeline"]["sideWidth"] - 40),
40 | FooterIconButton(
41 | icon: Icons.add,
42 | onPressed: () => controller.addConfigLayer(),
43 | tooltip: 'Add layer',
44 | ),
45 | FooterIconButton(
46 | icon: Icons.all_inclusive,
47 | onPressed: () => controller.setIsLooping(!controller.isLooping),
48 | tooltip:
49 | controller.isLooping ? 'Disable looping' : 'Enable looping',
50 | color: controller.isLooping ? Themes.activeColor : null,
51 | ),
52 | ],
53 | ),
54 | );
55 | },
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/editor/lib/display/header_view.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:editor/data/particular_editor_config.dart';
4 | import 'package:editor/data/particular_editor_controller.dart';
5 | import 'package:editor/data/particular_editor_layer.dart';
6 | import 'package:editor/services/io.dart';
7 | import 'package:editor/theme/theme.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:particular/particular.dart';
10 |
11 | /// The header line for the application that contains the buttons for layers.
12 | class HeaderView extends StatefulWidget {
13 | /// The configurations for the application.
14 | final Map appConfigs;
15 |
16 | /// The controller for the particle system.
17 | final ParticularEditorController controller;
18 |
19 | /// The callback function for when the background image is changed.
20 | final Function(Uint8List) onBackroundImageChanged;
21 |
22 | /// Creates a footer view.
23 | const HeaderView(
24 | {super.key,
25 | required this.appConfigs,
26 | required this.controller,
27 | required this.onBackroundImageChanged});
28 |
29 | @override
30 | State createState() => _HeaderViewState();
31 | }
32 |
33 | /// Creates a header view.
34 | class _HeaderViewState extends State {
35 | /// Creates a footer view.
36 | @override
37 | Widget build(BuildContext context) {
38 | return Container(
39 | padding: const EdgeInsets.symmetric(horizontal: 10),
40 | height: widget.appConfigs["appBarHeight"],
41 | color: Colors.white10,
42 | child: Row(
43 | children: [
44 | Image.asset("assets/favicon.png"),
45 | _menuButton(
46 | child: Icon(
47 | Icons.menu,
48 | color: Themes.foregroundColor,
49 | ),
50 | style: IconButton.styleFrom(backgroundColor: Colors.white10),
51 | items: {
52 | "Import configs": _importConfigs,
53 | "Import with textures (zipped)": _importConfigsWithTextures,
54 | "Export configs": _exportConfigs,
55 | "Export with textures (zipped)": _exportConfigsWithTextures,
56 | "Add background image": _browseBackgroundImage,
57 | },
58 | ),
59 | const Expanded(child: SizedBox()),
60 | _menuButton(
61 | child: const Text("Export"),
62 | items: {
63 | "Export configs": _exportConfigs,
64 | "Export with textures (zipped)": _exportConfigsWithTextures
65 | },
66 | ),
67 | ],
68 | ),
69 | );
70 | }
71 |
72 | /// Creates a menu button widget.
73 | ///
74 | /// The [child] parameter represents the child widget of the button.
75 | /// The [style] parameter represents the style of the button.
76 | /// The [items] parameter represents the map of items that will be displayed
77 | /// in the menu when the button is pressed.
78 | ///
79 | /// Returns a [MenuAnchor] widget that contains the button and the menu.
80 | Widget _menuButton({
81 | required Widget child,
82 | ButtonStyle? style,
83 | required Map items,
84 | }) {
85 | final entries = items.entries.toList();
86 | // Create a menu anchor widget that contains the button and the menu.
87 | return MenuAnchor(
88 | builder: (BuildContext context, MenuController controller,
89 | Widget? innerChild) {
90 | return ElevatedButton(
91 | style: style ?? Themes.buttonStyle(),
92 | child: child,
93 | onPressed: () {
94 | if (controller.isOpen) {
95 | controller.close();
96 | } else {
97 | controller.open();
98 | }
99 | },
100 | );
101 | },
102 | // Create a list of menu item buttons that represent the items in the menu.
103 | menuChildren: List.generate(
104 | entries.length,
105 | (int index) => MenuItemButton(
106 | onPressed: entries[index].value,
107 | child: Text(entries[index].key),
108 | ),
109 | ),
110 | );
111 | }
112 |
113 | /// Browse and import configs
114 | Future _importConfigs() async {
115 | final configs = await browseConfigs(["json"]);
116 | if (configs != null) {
117 | widget.controller.addConfigLayer(configsData: configs);
118 | }
119 | }
120 |
121 | /// Browse and import configs
122 | Future _importConfigsWithTextures() async {
123 | final files = await browseConfigsWithTexture();
124 | for (var entry in files.entries) {
125 | if (!entry.key.endsWith(".json")) {
126 | continue;
127 | }
128 | List configLayers = entry.value;
129 | for (var configLayer in configLayers) {
130 | final layer = ParticularEditorLayer(
131 | texture: files[configLayer["textureFileName"]].$1,
132 | textureBytes: files[configLayer["textureFileName"]].$2,
133 | configs: ParticularConfigs.initialize(configs: configLayer),
134 | );
135 | widget.controller.addParticularLayer(layer);
136 | }
137 | }
138 | }
139 |
140 | /// Save configs without textures
141 | void _exportConfigs() {
142 | var layersConfigs = [];
143 | for (var i = 0; i < widget.controller.layers.length; i++) {
144 | layersConfigs.add(widget.controller.layers[i].configs.toMap());
145 | }
146 | saveConfigs(configs: layersConfigs);
147 | }
148 |
149 | /// Save configs with textures (zipped)
150 | void _exportConfigsWithTextures() {
151 | final layersConfigs = [];
152 | final texures = {};
153 | for (var i = 0; i < widget.controller.layers.length; i++) {
154 | layersConfigs.add(widget.controller.layers[i].configs.toMap());
155 | var textureName = widget.controller.layers[i].configs.textureFileName;
156 |
157 | if (!texures.containsKey(textureName)) {
158 | texures[textureName] =
159 | (widget.controller.layers[i] as ParticularEditorLayer)
160 | .textureBytes!;
161 | }
162 | }
163 | saveConfigsWithTextures(configs: layersConfigs, textures: texures);
164 | }
165 |
166 | /// Browse background image
167 | void _browseBackgroundImage() async {
168 | final files = await browseFiles();
169 | if (files.isNotEmpty) {
170 | setState(() {
171 | widget.onBackroundImageChanged(files.first.bytes!);
172 | });
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/editor/lib/display/inspector_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:editor/data/inspector.dart';
2 | import 'package:editor/data/particular_editor_config.dart';
3 | import 'package:editor/data/particular_editor_controller.dart';
4 | import 'package:editor/data/particular_editor_layer.dart';
5 | import 'package:editor/theme/theme.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:flutter_colorpicker/flutter_colorpicker.dart';
8 | import 'package:intry/intry.dart';
9 | import 'package:particular/particular.dart';
10 |
11 | import '../services/io.dart';
12 |
13 | /// The inspector view for the application
14 | class InspectorView extends StatefulWidget {
15 | final Map appConfigs;
16 | final ParticularEditorController controller;
17 | const InspectorView({
18 | super.key,
19 | required this.appConfigs,
20 | required this.controller,
21 | });
22 |
23 | @override
24 | State createState() => _InspectorViewState();
25 | }
26 |
27 | class _InspectorViewState extends State {
28 | final _selectedColor = ValueNotifier(null);
29 | int _selectedTabIndex = 0;
30 | ParticularEditorLayer? _selectedLayer;
31 |
32 | @override
33 | void initState() {
34 | _selectTab(_selectedTabIndex);
35 | super.initState();
36 | }
37 |
38 | void _selectTab(int index) {
39 | _selectedTabIndex = index;
40 | final node = widget.appConfigs["inspector"]["components"][index];
41 | final children = [];
42 | for (var line in node["children"]) {
43 | children.add(Inspector(
44 | line["ui"] ?? "input",
45 | line["min"],
46 | line["max"],
47 | line["type"],
48 | line["title"] ?? "",
49 | line["inputs"] ?? {},
50 | ));
51 | }
52 | Inspector.list.value = InspectorList(node["title"], children);
53 | }
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | var themeData = Theme.of(context);
58 | return SizedBox(
59 | width: widget.appConfigs["inspector"]["width"],
60 | child: ListenableBuilder(
61 | listenable: widget.controller.getNotifier(NotifierType.layer),
62 | builder: (context, child) {
63 | _selectedLayer =
64 | widget.controller.selectedLayer as ParticularEditorLayer?;
65 | if (widget.controller.selectedLayer == null) {
66 | return const SizedBox();
67 | }
68 | return Column(
69 | crossAxisAlignment: CrossAxisAlignment.stretch,
70 | children: [
71 | _tabBarBuilder(themeData),
72 | _inspactorListBuilder(themeData),
73 | _colorPickerBuilder(),
74 | ],
75 | );
76 | },
77 | ),
78 | );
79 | }
80 |
81 | Widget _tabBarBuilder(ThemeData themeData) {
82 | return ValueListenableBuilder(
83 | valueListenable: Inspector.list,
84 | builder: (context, value, child) {
85 | return Container(
86 | margin: const EdgeInsets.only(bottom: 10),
87 | color: Colors.black12,
88 | child: Row(
89 | children: [
90 | _tabItemBuilder(themeData, 0, "Emitter"),
91 | _tabItemBuilder(themeData, 1, "Particle"),
92 | ],
93 | ),
94 | );
95 | },
96 | );
97 | }
98 |
99 | Widget _tabItemBuilder(ThemeData themeData, int index, String title) {
100 | return Expanded(
101 | child: InkWell(
102 | child: Container(
103 | alignment: Alignment.center,
104 | height: widget.appConfigs["appBarHeight"],
105 | color:
106 | _selectedTabIndex != index ? Theme.of(context).splashColor : null,
107 | child: Text(title, style: themeData.textTheme.bodyLarge),
108 | ),
109 | onTap: () => _selectTab(index),
110 | ),
111 | );
112 | }
113 |
114 | Widget _inspactorListBuilder(ThemeData themeData) {
115 | return ValueListenableBuilder(
116 | valueListenable: Inspector.list,
117 | builder: (context, value, child) {
118 | return Expanded(
119 | child: ListView.builder(
120 | itemCount: value.children.length,
121 | itemBuilder: (context, index) =>
122 | _inspectorItemBuilder(themeData, value.children[index]),
123 | ),
124 | );
125 | },
126 | );
127 | }
128 |
129 | Widget _inspectorItemBuilder(ThemeData themeData, Inspector inspector) {
130 | if (inspector.type == null ||
131 | inspector.type ==
132 | [
133 | "gravity",
134 | "radial"
135 | ][_selectedLayer!.configs.getParam("emitterType").index]) {
136 | var items = [];
137 | _inputLineBuilder(
138 | inspector,
139 | items,
140 | themeData,
141 | (themeData, inspector, entry) =>
142 | _addInputs(themeData, inspector, entry),
143 | );
144 |
145 | return Column(
146 | children: [
147 | Padding(
148 | padding: const EdgeInsets.symmetric(horizontal: 12),
149 | child: Column(
150 | crossAxisAlignment: CrossAxisAlignment.start,
151 | children: [
152 | inspector.title.isEmpty
153 | ? const SizedBox()
154 | : Text(inspector.title,
155 | style: themeData.textTheme.titleMedium),
156 | const SizedBox(height: 2),
157 | Row(children: items),
158 | ],
159 | ),
160 | ),
161 | items.isEmpty ? const SizedBox(height: 12) : const Divider(height: 14)
162 | ],
163 | );
164 | }
165 | return const SizedBox();
166 | }
167 |
168 | void _inputLineBuilder(
169 | Inspector inspector,
170 | List children,
171 | ThemeData themeData,
172 | Widget Function(ThemeData themeData, Inspector inspector,
173 | MapEntry entry)
174 | inspectorBuilder,
175 | ) {
176 | final entries = inspector.inputs.entries.toList();
177 | for (var i = 0; i < entries.length; i++) {
178 | var entry = entries[i];
179 | children.add(_getText(entry.key.toTitleCase(), themeData));
180 | children.add(const SizedBox(width: 8));
181 | children.add(
182 | Expanded(
183 | child: ListenableBuilder(
184 | listenable: _selectedLayer!.configs.getNotifier(entry.value),
185 | builder: (c, w) => inspectorBuilder(themeData, inspector, entry),
186 | ),
187 | ),
188 | );
189 | if (i < entries.length - 1) {
190 | children.add(const SizedBox(width: 20));
191 | }
192 | }
193 | }
194 |
195 | Widget _addInputs(ThemeData themeData, Inspector inspector,
196 | MapEntry entry) {
197 | if (inspector.ui == "input") {
198 | return IntryNumericField(
199 | slidingSpeed: 1,
200 | min: inspector.min,
201 | max: inspector.max,
202 | decoration: IntryFieldDecoration.outline(context),
203 | value: _selectedLayer!.configs.getParam(entry.value).toDouble(),
204 | onChanged: (double value) => _updateParticleParam(entry.value, value),
205 | );
206 | } else if (inspector.ui == "dropdown") {
207 | List values = switch (entry.value) {
208 | "blendFunctionSource" ||
209 | "blendFunctionDestination" =>
210 | BlendFunction.values,
211 | "renderBlendMode" || "textureBlendMode" => BlendMode.values,
212 | _ => EmitterType.values,
213 | };
214 | var items = values
215 | .map((item) => DropdownMenuItem(
216 | alignment: Alignment.center,
217 | value: item,
218 | child: _getText(
219 | item.toString().split('.').last.toTitleCase(), themeData)))
220 | .toList();
221 | return DropdownButtonFormField(
222 | decoration: const InputDecoration(
223 | contentPadding: EdgeInsets.symmetric(horizontal: 12),
224 | border: OutlineInputBorder(),
225 | ),
226 | itemHeight: 48,
227 | items: items,
228 | value: _selectedLayer!.configs.getParam(entry.value),
229 | onChanged: (dynamic selected) {
230 | _selectedLayer!.configs.updateWith({entry.value: selected});
231 | if (entry.value == "emitterType") {
232 | setState(() {});
233 | }
234 | },
235 | );
236 | } else if (inspector.ui == "color") {
237 | return ElevatedButton(
238 | style: Themes.buttonStyle(
239 | color: _selectedLayer!.configs.getParam(entry.value).getColor()),
240 | onPressed: () => _selectedColor.value = entry.value,
241 | child: const SizedBox(),
242 | );
243 | } else {
244 | return ElevatedButton(
245 | style: Themes.buttonStyle(),
246 | child: _getText("${entry.value}".toTitleCase(), themeData),
247 | onPressed: () async {
248 | final result = await browseImage();
249 | if (result.$2 != null) {
250 | _selectedLayer!.configs.update(textureFileName: result.$1);
251 | _selectedLayer!.texture = result.$2!;
252 | _selectedLayer!.textureBytes = result.$3!;
253 | }
254 | },
255 | );
256 | }
257 | }
258 |
259 | Text _getText(String text, ThemeData themeData) =>
260 | Text(text, style: themeData.textTheme.labelMedium);
261 |
262 | void _updateParticleParam(String key, num value) {
263 | var param = _selectedLayer!.configs.getParam(key);
264 | _selectedLayer!.configs
265 | .updateWith({key: param is int ? value.toInt() : value});
266 | }
267 |
268 | Widget _colorPickerBuilder() {
269 | return ValueListenableBuilder(
270 | valueListenable: _selectedColor,
271 | builder: (context, value, child) {
272 | if (value == null) {
273 | return const SizedBox();
274 | }
275 | return TapRegion(
276 | onTapOutside: (event) => _selectedColor.value = null,
277 | child: Container(
278 | color: Colors.black12,
279 | padding: const EdgeInsets.all(16),
280 | child: SlidePicker(
281 | showIndicator: false,
282 | showSliderText: false,
283 | pickerColor: _selectedLayer!.configs.getParam(value).getColor(),
284 | onColorChanged: (color) {
285 | _selectedLayer!.configs.updateWith(
286 | {value: ARGB(color.a, color.r, color.g, color.b)});
287 | },
288 | ),
289 | ),
290 | );
291 | },
292 | );
293 | }
294 | }
295 |
296 | extension StringExtension on String {
297 | String toTitleCase() => "${this[0].toUpperCase()}${substring(1)}";
298 | }
299 |
--------------------------------------------------------------------------------
/editor/lib/display/timeline_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:editor/data/particular_editor_config.dart';
2 | import 'package:editor/data/particular_editor_controller.dart';
3 | import 'package:editor/services/io.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:intry/intry.dart';
6 | import 'package:particular/particular.dart';
7 |
8 | import 'widgets/footer_icon_button.dart';
9 |
10 | /// The timeline view for application.
11 | class TimelineView extends StatefulWidget {
12 | final Map appConfigs;
13 | final ParticularEditorController controller;
14 |
15 | const TimelineView({
16 | super.key,
17 | required this.appConfigs,
18 | required this.controller,
19 | });
20 |
21 | @override
22 | State createState() => _TimelineViewState();
23 | }
24 |
25 | class _TimelineViewState extends State {
26 | /// Creates a timeline view.
27 | @override
28 | Widget build(BuildContext context) {
29 | var controller = widget.controller;
30 | return SizedBox(
31 | height: widget.appConfigs["timeline"]["height"],
32 | child: ListenableBuilder(
33 | listenable: controller.getNotifier(NotifierType.layer),
34 | builder: (context, child) {
35 | return Stack(
36 | children: [
37 | Container(
38 | color: Colors.black12,
39 | width: widget.appConfigs["timeline"]["sideWidth"],
40 | ),
41 | ReorderableListView.builder(
42 | buildDefaultDragHandles: false,
43 | itemBuilder: (c, i) =>
44 | _layerItemBuilder(controller.layers.length - i - 1),
45 | itemCount: controller.layers.length,
46 | onReorder: (int oldIndex, int newIndex) =>
47 | controller.reOrderLayer(oldIndex, newIndex),
48 | ),
49 | _timeSeekBarBuilder(),
50 | ],
51 | );
52 | },
53 | ));
54 | }
55 |
56 | /// Builds a layer item.
57 | Widget _layerItemBuilder(int index) {
58 | final key = Key('$index');
59 | final layer = widget.controller.layers[index];
60 | return Container(
61 | key: key,
62 | height: widget.appConfigs["timeline"]["layerHeight"],
63 | margin: const EdgeInsets.symmetric(vertical: 1),
64 | color: Colors.black26,
65 | child: GestureDetector(
66 | child: Row(
67 | crossAxisAlignment: CrossAxisAlignment.stretch,
68 | children: [
69 | Container(
70 | color: widget.controller.selectedLayerIndex == index
71 | ? Colors.white30
72 | : Colors.white12,
73 | width: widget.appConfigs["timeline"]["sideWidth"],
74 | child: Row(
75 | children: [
76 | const SizedBox(width: 8),
77 | Tooltip(
78 | message: 'Drag to reorder',
79 | child: ReorderableDragStartListener(
80 | key: key,
81 | index: index,
82 | child: const Icon(Icons.drag_handle, size: 12),
83 | ),
84 | ),
85 | const SizedBox(width: 8),
86 | Expanded(
87 | child: ListenableBuilder(
88 | listenable: layer.configs.getNotifier("configName"),
89 | builder: (context, child) {
90 | return IntryTextField(
91 | value: layer.configs.configName,
92 | onChanged: (value) =>
93 | layer.configs.updateWith({"configName": value}),
94 | );
95 | },
96 | ),
97 | ),
98 | FooterIconButton(
99 | icon: Icons.save,
100 | onPressed: () => saveConfigs(
101 | configs: layer.configs.toMap(),
102 | filename: layer.configs.configName,
103 | ),
104 | tooltip: 'Export layer',
105 | ),
106 | FooterIconButton(
107 | icon: Icons.delete,
108 | onPressed: () => widget.controller.removeLayerAt(index),
109 | tooltip: 'Delete layer',
110 | ),
111 | /* _footerItem(
112 | controllers.selected!.isVisible
113 | ? Icons.visibility
114 | : Icons.visibility_off,
115 | () => controllers.toggleVisible(controllers.selectedIndex),
116 | ), */
117 | ],
118 | ),
119 | ),
120 | _activeLineBuilder(layer.configs),
121 | ],
122 | ),
123 | onTap: () => widget.controller.selectLayerAt(index),
124 | ),
125 | );
126 | }
127 |
128 | Widget _activeLineBuilder(ParticularConfigs configs) {
129 | var c = widget.controller;
130 | return ListenableBuilder(
131 | listenable: c.getNotifier(NotifierType.time),
132 | builder: (context, child) {
133 | var end = configs.endTime < 0 ? c.timelineDuration : configs.endTime;
134 | var duration = end - configs.startTime;
135 | var emptyArea = c.timelineDuration - duration;
136 | var positionRate = emptyArea <= 0 ? 0 : configs.startTime / emptyArea;
137 | return Expanded(
138 | child: SizedBox(
139 | child: FractionallySizedBox(
140 | alignment: Alignment(positionRate * 2 - 1, 0),
141 | widthFactor: duration / c.timelineDuration,
142 | heightFactor: 0.3,
143 | child: Container(color: Colors.green),
144 | ),
145 | ),
146 | );
147 | },
148 | );
149 | }
150 |
151 | Widget _timeSeekBarBuilder() {
152 | var c = widget.controller;
153 | return Positioned(
154 | right: 1,
155 | left: widget.appConfigs["timeline"]["sideWidth"],
156 | child: ListenableBuilder(
157 | listenable: widget.controller.getNotifier(NotifierType.time),
158 | builder: (context, child) {
159 | var timeRatio =
160 | c.elapsedTime.clamp(0, c.timelineDuration) / c.timelineDuration;
161 | return Align(
162 | alignment: Alignment(timeRatio * 2 - 1, 0),
163 | child: Container(
164 | width: 1,
165 | color: Colors.white,
166 | height: widget.appConfigs["timeline"]["height"],
167 | ),
168 | );
169 | },
170 | ),
171 | );
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/editor/lib/display/widgets/footer_icon_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// This class creates an [IconButton] widget for the footer of the
4 | /// screen. The icon button represents an action and when pressed, it
5 | /// executes the provided function [onPressed].
6 | class FooterIconButton extends StatelessWidget {
7 | const FooterIconButton({
8 | super.key,
9 | required this.icon,
10 | this.onPressed,
11 | this.tooltip,
12 | this.color,
13 | });
14 |
15 | final IconData icon;
16 | final VoidCallback? onPressed;
17 | final String? tooltip;
18 | final Color? color;
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return Tooltip(
23 | message: tooltip ?? "",
24 | child: IconButton(
25 | padding: const EdgeInsets.all(2),
26 | icon: Icon(
27 | icon,
28 | size: 16,
29 | color: color,
30 | ),
31 | onPressed: onPressed,
32 | ),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/editor/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:editor/data/particular_editor_controller.dart';
4 | import 'package:editor/display/footer_view.dart';
5 | import 'package:editor/display/header_view.dart';
6 | import 'package:editor/display/inspector_view.dart';
7 | import 'package:editor/display/timeline_view.dart';
8 | import 'package:editor/theme/theme.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:flutter/services.dart';
11 | import 'package:particular/particular.dart';
12 |
13 | void main() {
14 | runApp(const EditorApp());
15 | }
16 |
17 | class EditorApp extends StatefulWidget {
18 | const EditorApp({super.key});
19 |
20 | @override
21 | State createState() => _EditorAppState();
22 | }
23 |
24 | class _EditorAppState extends State {
25 | Uint8List? _backgroundImage;
26 | Map _appConfigs = {};
27 | final ParticularEditorController _particleController =
28 | ParticularEditorController();
29 |
30 | @override
31 | void initState() {
32 | _loadInitialConfigs();
33 | super.initState();
34 | }
35 |
36 | /// Loads the initial configurations for the application.
37 | ///
38 | /// This function does not have any parameters and does not return any value.
39 | void _loadInitialConfigs() async {
40 | final json = await rootBundle.loadString("assets/app_configs.json");
41 | _appConfigs = Map.castFrom(jsonDecode(json));
42 | setState(() {});
43 |
44 | // Add sample emitter
45 | await _particleController.addConfigLayer();
46 | await Future.delayed(const Duration(milliseconds: 100));
47 | if (mounted) {
48 | final size = MediaQuery.of(context).size;
49 | _particleController.selectedLayer!.configs.update(
50 | emitterX: size.width * 0.5 - _appConfigs["inspector"]["width"] * 0.5,
51 | emitterY: (size.height +
52 | _appConfigs["appBarHeight"] -
53 | _appConfigs["timeline"]["height"] -
54 | _appConfigs["footerHeight"]) *
55 | 0.5);
56 | }
57 | }
58 |
59 | @override
60 | Widget build(BuildContext context) {
61 | if (_appConfigs.isEmpty) {
62 | return const Center(child: CircularProgressIndicator());
63 | }
64 | return MaterialApp(
65 | title: "Particular Editor",
66 | theme: customTheme,
67 | home: Scaffold(
68 | body: Row(
69 | crossAxisAlignment: CrossAxisAlignment.stretch,
70 | children: [
71 | Expanded(
72 | child: Column(
73 | crossAxisAlignment: CrossAxisAlignment.stretch,
74 | children: [
75 | HeaderView(
76 | appConfigs: _appConfigs,
77 | controller: _particleController,
78 | onBackroundImageChanged: (image) {
79 | setState(() => _backgroundImage = image);
80 | },
81 | ),
82 | _canvasBuilder(),
83 | FooterView(
84 | appConfigs: _appConfigs,
85 | controller: _particleController,
86 | ),
87 | TimelineView(
88 | appConfigs: _appConfigs,
89 | controller: _particleController,
90 | ),
91 | ],
92 | ),
93 | ),
94 | InspectorView(
95 | appConfigs: _appConfigs,
96 | controller: _particleController,
97 | ),
98 | ],
99 | ),
100 | ),
101 | );
102 | }
103 |
104 | Widget _canvasBuilder() {
105 | return Expanded(
106 | child: GestureDetector(
107 | onPanUpdate: (details) {
108 | _particleController.selectedLayer?.configs.updateWith({
109 | "emitterX": details.localPosition.dx,
110 | "emitterY": details.localPosition.dy
111 | });
112 | },
113 | onTapDown: (details) {
114 | _particleController.resetTick();
115 | _particleController.selectedLayer?.configs.updateWith({
116 | "emitterX": details.localPosition.dx,
117 | "emitterY": details.localPosition.dy
118 | });
119 | },
120 | child: Container(
121 | clipBehavior: Clip.hardEdge,
122 | decoration: BoxDecoration(
123 | shape: BoxShape.rectangle,
124 | color: Colors.black,
125 | image: _backgroundImage == null
126 | ? null
127 | : DecorationImage(image: MemoryImage(_backgroundImage!))),
128 | child: Particular(
129 | controller: _particleController,
130 | ),
131 | ),
132 | ),
133 | );
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/editor/lib/services/downloader/file_saver_io.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:file_picker/file_picker.dart';
4 |
5 | class FileSaver {
6 | /// Saves a file to the user's device.
7 | /// Returns the saved file path.
8 | static Future saveFile({
9 | required String title,
10 | required Uint8List bytes,
11 | required String filename,
12 | }) =>
13 | FilePicker.platform.saveFile(
14 | bytes: bytes,
15 | dialogTitle: title,
16 | fileName: filename,
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/editor/lib/services/downloader/file_saver_none.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | class FileSaver {
4 | /// Saves a file to the user's device.
5 | /// Returns the saved file path.
6 | static Future saveFile({
7 | required String title,
8 | required Uint8List bytes,
9 | required String filename,
10 | }) => throw UnimplementedError('It is not implemented');
11 | }
12 |
--------------------------------------------------------------------------------
/editor/lib/services/downloader/file_saver_web.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:web/web.dart' as web;
5 |
6 | class FileSaver {
7 | /// Saves a file to the user's device.
8 | static Future saveFile({
9 | required String title,
10 | required Uint8List bytes,
11 | required String filename,
12 | }) async {
13 | assert(kIsWeb || kIsWasm);
14 | final anchor = web.document.createElement('a') as web.HTMLAnchorElement
15 | ..href = "data:application/octet-stream;base64,${base64Encode(bytes)}"
16 | ..style.display = 'none'
17 | ..download = filename;
18 |
19 | web.document.body!.appendChild(anchor);
20 | anchor.click();
21 | web.document.body!.removeChild(anchor);
22 | return null;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/editor/lib/services/io.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | // ignore: avoid_web_libraries_in_flutter
3 | import 'dart:typed_data';
4 | import 'dart:ui' as ui;
5 |
6 | import 'package:archive/archive.dart';
7 | import 'package:file_picker/file_picker.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:particular/particular.dart';
10 |
11 | import 'downloader/file_saver_none.dart'
12 | if (dart.library.io) 'downloader/file_saver_io.dart'
13 | if (dart.library.js_interop) 'downloader/file_saver_web.dart';
14 |
15 | /// Browse files from the user's device.
16 | ///
17 | /// This function uses the [FilePicker] library to allow the user to select
18 | /// files. It returns a `Future` that resolves to a list of [PlatformFile]
19 | /// objects. If the user cancels the selection, the function returns an empty
20 | /// list.
21 | ///
22 | /// Returns:
23 | /// - A `Future>`: A list of [PlatformFile] objects
24 | /// representing the selected files.
25 | Future> browseFiles() async {
26 | // Use the FilePicker library to allow the user to select files.
27 | final pickResult = await FilePicker.platform.pickFiles(
28 | withData: true,
29 | type: FileType.any,
30 | );
31 |
32 | // If the user cancels the selection or no file is selected, return an empty
33 | // list. Otherwise, return the selected files.
34 | return pickResult?.files ?? [];
35 | }
36 |
37 | /// Browse and load an image from the user's device.
38 | ///
39 | /// This function uses the [FilePicker] library to allow the user to select an
40 | /// image file. If the user cancels the selection or no image is selected, the
41 | /// function returns a `Future` that resolves to a tuple containing the empty
42 | /// string and `null`. Otherwise, it reads the contents of the selected file,
43 | /// decodes it from bytes and returns a `Future` that resolves to a tuple
44 | /// containing the name of the selected file and the decoded image.
45 | ///
46 | /// Returns:
47 | /// - A `Future<(String, ui.Image?)>`: A tuple containing the name of the
48 | /// selected file and the decoded image. If the user cancels the selection or
49 | /// no image is selected, the tuple contains the empty string and `null`.
50 | Future<(String, ui.Image?, Uint8List?)> browseImage() async {
51 | // Use the FilePicker library to allow the user to select an image file.
52 | final files = await browseFiles();
53 |
54 | if (files.isNotEmpty) {
55 | PlatformFile file = files.first;
56 | if (file.bytes != null) {
57 | var image = await loadUIImage(file.bytes!);
58 | return (file.name, image, file.bytes);
59 | }
60 | }
61 |
62 | return ("", null, null);
63 | }
64 |
65 | /// Browse and load configs from a file with specified extensions.
66 | ///
67 | /// The function uses the [FilePicker] library to allow the user to select a file
68 | /// with a specific set of extensions. If the user cancels the selection or no
69 | /// file is selected, the function returns `null`. Otherwise, it reads the
70 | /// contents of the selected file, decodes it from JSON and returns the decoded
71 | /// map.
72 | ///
73 | /// Parameters:
74 | /// - [extensions]: A list of file extensions supported by the config file.
75 | ///
76 | /// Returns:
77 | /// - A `Future`: A map of configuration data, decoded
78 | /// from JSON, or `null` if no file was selected.
79 | Future browseConfigs(List extensions) async {
80 | // Use the FilePicker library to allow the user to select a config file.
81 | FilePickerResult? result = await FilePicker.platform.pickFiles(
82 | withData: true, // Request file contents.
83 | type: FileType.custom, // Allow any type of file.
84 | allowedExtensions:
85 | extensions, // Only allow files with specified extensions.
86 | );
87 |
88 | // If no file was selected, return null.
89 | if (result == null) return null;
90 |
91 | // Get the first selected file.
92 | PlatformFile file = result.files.first;
93 |
94 | // Decode the JSON contents of the file.
95 | String json = String.fromCharCodes(file.bytes!);
96 | return jsonDecode(json);
97 | }
98 |
99 | /// Browse and load configs and textures from a zip file.
100 | ///
101 | /// The function uses the [FilePicker] library to allow the user to select a zip
102 | /// file. If the user cancels the selection or no file is selected, the function
103 | /// returns an empty map. Otherwise, it extracts the contents of the selected zip
104 | /// file to disk, and returns a map of the extracted files.
105 | ///
106 | /// Returns:
107 | /// - A `Map`: A map of extracted files from the zip
108 | Future> browseConfigsWithTexture() async {
109 | // Use the FilePicker library to allow the user to select a config file.
110 | FilePickerResult? result = await FilePicker.platform.pickFiles(
111 | withData: true, // Request file contents.
112 | type: FileType.custom, // Allow any type of file.
113 | allowedExtensions: ["zip"], // Only allow files with specified extensions.
114 | );
115 |
116 | // If no file was selected, return null.
117 | if (result == null) return {};
118 |
119 | // Decode the Zip file
120 | final archive = ZipDecoder().decodeBytes(result.files.first.bytes!);
121 |
122 | // Extract the contents of the Zip archive to disk.
123 | final files = {};
124 | for (final file in archive) {
125 | if (file.name.endsWith(".json")) {
126 | files[file.name] = jsonDecode(String.fromCharCodes(file.content));
127 | } else {
128 | var image = await loadUIImage(file.content);
129 | files[file.name] = (image, file.content);
130 | }
131 | }
132 | return files;
133 | }
134 |
135 | /// Save the provided configs to a file.
136 | ///
137 | /// If the app is running on a non-web platform, the function uses the
138 | /// [FilePicker.saveFile] method to open a file picker dialog for the user to
139 | /// select a filename. The configs are encoded to JSON and saved to the
140 | /// selected file with the `.json` extension.
141 | ///
142 | /// Parameters:
143 | /// - [configs]: The configs to save.
144 | /// - [filename]: The name of the file to save the configs to. If not
145 | /// provided, the filename will be "configs".
146 | ///
147 | /// Returns:
148 | /// - A `Future`: A future that completes when the configs have been
149 | /// saved to a file.
150 | Future saveConfigs({
151 | required dynamic configs,
152 | String? filename,
153 | }) async {
154 | final json = jsonEncode(configs);
155 | debugPrint(json);
156 | await FileSaver.saveFile(
157 | bytes: utf8.encode(json),
158 | title: "Save Particle Configs",
159 | filename: "${filename ?? "configs"}.json",
160 | );
161 | }
162 |
163 | /// Saves the provided [configs] and [textures] to a zipped file.
164 | ///
165 | /// If the app is running on a non-web platform, it uses the [FilePicker.saveFile]
166 | /// method to open a file picker dialog for the user to select a filename.
167 | ///
168 | /// The [configs] and [textures] are encoded into a zip file and saved to the
169 | /// selected file with the `.zip` extension.
170 | ///
171 | /// Parameters:
172 | /// - [configs]: The configs to save.
173 | /// - [textures]: The textures to save.
174 | /// - [filename]: The name of the file to save the configs to. If not
175 | /// provided, the filename will be "configs".
176 | ///
177 | /// Returns:
178 | /// A `Future`: A future that completes when the configs and textures
179 | /// have been saved to a file.
180 | Future saveConfigsWithTextures({
181 | required dynamic configs,
182 | required Map textures,
183 | String? filename,
184 | }) async {
185 | // Create ZipEncoder and AichiveFile instances.
186 | final encoder = ZipEncoder();
187 | final archive = Archive();
188 |
189 | // Add the configs.json to the archive.
190 | // Convert the configs to JSON and encode it into bytes.
191 | final json = jsonEncode(configs);
192 | final jbytes = utf8.encode(json);
193 |
194 | // Create a new ArchiveFile instance with the name 'configs.json' and the
195 | // encoded JSON bytes as the file contents.
196 | final archiveFile = ArchiveFile('configs.json', jbytes.length, jbytes);
197 | archive.addFile(archiveFile);
198 |
199 | // Add the textures into the archive.
200 | for (var entry in textures.entries) {
201 | archive.addFile(ArchiveFile(entry.key, entry.value.length, entry.value));
202 | }
203 |
204 | // Create an OutputStream with little endian byte order.
205 | final outputStream = OutputStream(byteOrder: LITTLE_ENDIAN);
206 |
207 | // Encode the archive into bytes with the highest compression level.
208 | final bytes = encoder.encode(archive,
209 | level: Deflate.BEST_COMPRESSION, output: outputStream);
210 |
211 | // Save the encoded bytes to a file
212 | await FileSaver.saveFile(
213 | bytes: Uint8List.fromList(bytes!),
214 | title: "Save Particle Configs",
215 | filename: "${filename ?? "configs"}.zip",
216 | );
217 | }
218 |
--------------------------------------------------------------------------------
/editor/lib/theme/theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class Themes {
4 | static Color backgroundColor = const Color(0xFF2C2C2C);
5 | static Color foregroundColor = const Color(0xFF9E9E9E);
6 | static Color activeColor = Colors.lightBlueAccent;
7 | static ButtonStyle buttonStyle({Color? color}) {
8 | return ElevatedButton.styleFrom(
9 | shadowColor: Colors.transparent,
10 | foregroundColor: foregroundColor,
11 | shape: RoundedRectangleBorder(
12 | borderRadius: BorderRadius.circular(4),
13 | side: BorderSide(color: foregroundColor),
14 | ),
15 | backgroundColor: color ?? backgroundColor,
16 | );
17 | }
18 | }
19 |
20 | ThemeData get customTheme => ThemeData(
21 | useMaterial3: true,
22 | brightness: Brightness.dark,
23 | primarySwatch: Colors.grey,
24 | appBarTheme: AppBarTheme(
25 | backgroundColor: Themes.backgroundColor,
26 | titleTextStyle: TextStyle(
27 | fontSize: 12,
28 | fontWeight: FontWeight.w600,
29 | color: Themes.foregroundColor)),
30 | scaffoldBackgroundColor: Themes.backgroundColor,
31 | textTheme: TextTheme(
32 | labelLarge: TextStyle(
33 | fontSize: 12,
34 | fontWeight: FontWeight.w300,
35 | color: Themes.foregroundColor),
36 | labelMedium: TextStyle(
37 | fontSize: 10,
38 | fontWeight: FontWeight.w300,
39 | color: Themes.foregroundColor),
40 | labelSmall: TextStyle(
41 | fontSize: 8,
42 | fontWeight: FontWeight.w300,
43 | color: Themes.foregroundColor),
44 | titleLarge: TextStyle(
45 | fontSize: 12,
46 | fontWeight: FontWeight.w600,
47 | color: Themes.foregroundColor),
48 | titleMedium: TextStyle(
49 | fontSize: 10,
50 | fontWeight: FontWeight.w600,
51 | color: Themes.foregroundColor),
52 | titleSmall: TextStyle(
53 | fontSize: 8,
54 | fontWeight: FontWeight.w600,
55 | color: Themes.foregroundColor),
56 | bodyLarge: const TextStyle(fontSize: 12),
57 | bodyMedium: const TextStyle(fontSize: 10),
58 | bodySmall: const TextStyle(fontSize: 9),
59 | ),
60 | );
61 |
--------------------------------------------------------------------------------
/editor/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: editor
2 | description: "Demonstrates how to use the particular package."
3 | publish_to: 'none'
4 |
5 | version: 0.1.0+100
6 |
7 | environment:
8 | sdk: '>=3.3.1 <4.0.0'
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 | archive: ^3.6.1
13 | file_picker: ^8.0.6
14 | flutter_colorpicker: ^1.1.0
15 | intry: ^0.3.2
16 | particular:
17 | path: ../
18 | web: ^1.1.0
19 |
20 | dev_dependencies:
21 | integration_test:
22 | sdk: flutter
23 | flutter_test:
24 | sdk: flutter
25 | flutter_lints: ^4.0.0
26 |
27 | flutter:
28 | uses-material-design: true
29 | assets:
30 | - assets/
--------------------------------------------------------------------------------
/editor/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 in the flutter_test package. 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:editor/main.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:flutter_test/flutter_test.dart';
11 |
12 | void main() {
13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
14 | // Build our app and trigger a frame.
15 | await tester.pumpWidget(const EditorApp());
16 |
17 | // Verify that our counter starts at 0.
18 | expect(find.text('0'), findsOneWidget);
19 | expect(find.text('1'), findsNothing);
20 |
21 | // Tap the '+' icon and trigger a frame.
22 | await tester.tap(find.byIcon(Icons.add));
23 | await tester.pump();
24 |
25 | // Verify that our counter has incremented.
26 | expect(find.text('0'), findsNothing);
27 | expect(find.text('1'), findsOneWidget);
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/editor/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/web/favicon.png
--------------------------------------------------------------------------------
/editor/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/editor/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/editor/web/icons/Icon-maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/web/icons/Icon-maskable-192.png
--------------------------------------------------------------------------------
/editor/web/icons/Icon-maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/editor/web/icons/Icon-maskable-512.png
--------------------------------------------------------------------------------
/editor/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | editor
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/editor/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "editor",
3 | "short_name": "editor",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Symbolication related
35 | app.*.symbols
36 |
37 | # Obfuscation related
38 | app.*.map.json
39 |
40 | # Android Studio will place build artifacts here
41 | /android/app/debug
42 | /android/app/profile
43 | /android/app/release
44 |
--------------------------------------------------------------------------------
/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 and should not be manually edited.
5 |
6 | version:
7 | revision: "ba393198430278b6595976de84fe170f553cc728"
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: ba393198430278b6595976de84fe170f553cc728
17 | base_revision: ba393198430278b6595976de84fe170f553cc728
18 | - platform: android
19 | create_revision: ba393198430278b6595976de84fe170f553cc728
20 | base_revision: ba393198430278b6595976de84fe170f553cc728
21 |
22 | # User provided section
23 |
24 | # List of Local paths (relative to this file) that should be
25 | # ignored by the migrate tool.
26 | #
27 | # Files that are not part of the templates will be ignored by default.
28 | unmanaged_files:
29 | - 'lib/main.dart'
30 | - 'ios/Runner.xcodeproj/project.pbxproj'
31 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Demonstrates how to use the particular plugin.
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://docs.flutter.dev/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
13 |
14 | For help getting started with Flutter development, view the
15 | [online documentation](https://docs.flutter.dev/), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/example/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 https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/example/assets/firework.json:
--------------------------------------------------------------------------------
1 | {
2 | "emitterType": 0,
3 | "startColorAlpha": 1,
4 | "startColorRed": 0.5,
5 | "startColorGreen": 0.5,
6 | "startColorBlue": 0.5,
7 | "startColorVarianceAlpha": 0,
8 | "startColorVarianceRed": 0.44,
9 | "startColorVarianceGreen": 0.44,
10 | "startColorVarianceBlue": 0.44,
11 | "finishColorAlpha": 0.5,
12 | "finishColorRed": 0.5,
13 | "finishColorGreen": 0.5,
14 | "finishColorBlue": 0.5,
15 | "finishColorVarianceAlpha": 0,
16 | "finishColorVarianceRed": 0.5,
17 | "finishColorVarianceGreen": 0.5,
18 | "finishColorVarianceBlue": 0.5,
19 | "startParticleSize": 15,
20 | "startParticleSizeVariance": 10,
21 | "finishParticleSize": -1,
22 | "finishParticleSizeVariance": 0,
23 | "sourcePositionVariancex": 0,
24 | "sourcePositionVariancey": 0,
25 | "particleLifespan": 1.6,
26 | "particleLifespanVariance": 0.2,
27 | "duration": 0.1,
28 | "maxParticles": 2000,
29 | "speed": 82,
30 | "speedVariance": 271,
31 | "angle": -90,
32 | "angleVariance": 360,
33 | "gravityx": 0,
34 | "gravityy": 300,
35 | "rotationStart": 0,
36 | "rotationStartVariance": 0,
37 | "rotationEnd": 0,
38 | "rotationEndVariance": 0,
39 | "rotatePerSecond": 0,
40 | "rotatePerSecondVariance": 0,
41 | "maxRadius": 0,
42 | "maxRadiusVariance": 0,
43 | "minRadius": 0,
44 | "minRadiusVariance": 0,
45 | "radialAcceleration": 0,
46 | "radialAccelVariance": 0,
47 | "tangentialAcceleration": 0,
48 | "tangentialAccelVariance": 0,
49 | "renderBlendMode": 6,
50 | "textureBlendMode": 12,
51 | "yCoordFlipped": -1,
52 | "textureImageData": "",
53 | "configName": "fire",
54 | "textureFileName": "texture.png"
55 | }
--------------------------------------------------------------------------------
/example/assets/galaxy.json:
--------------------------------------------------------------------------------
1 | {
2 | "emitterType": 1,
3 | "startColorAlpha": 0.64,
4 | "startColorRed": 1,
5 | "startColorGreen": 1,
6 | "startColorBlue": 1,
7 | "startColorVarianceAlpha": 0.27,
8 | "startColorVarianceRed": 0.12,
9 | "startColorVarianceGreen": 0.15,
10 | "startColorVarianceBlue": 0.17,
11 | "finishColorAlpha": 0,
12 | "finishColorRed": 1,
13 | "finishColorGreen": 1,
14 | "finishColorBlue": 1,
15 | "finishColorVarianceAlpha": 0,
16 | "finishColorVarianceRed": 0,
17 | "finishColorVarianceGreen": 0,
18 | "finishColorVarianceBlue": 0,
19 | "startParticleSize": 16,
20 | "startParticleSizeVariance": 12,
21 | "finishParticleSize": 0,
22 | "finishParticleSizeVariance": 0,
23 | "sourcePositionVariancex": 11,
24 | "sourcePositionVariancey": 11,
25 | "particleLifespan": 0.722,
26 | "particleLifespanVariance": 0,
27 | "duration": -1,
28 | "maxParticles": 664,
29 | "speed": 0,
30 | "speedVariance": 0,
31 | "angle": 180,
32 | "angleVariance": 180,
33 | "gravityx": 0,
34 | "gravityy": 0,
35 | "rotationStart": 0,
36 | "rotationStartVariance": 0,
37 | "rotationEnd": 0,
38 | "rotationEndVariance": 0,
39 | "rotatePerSecond": -30,
40 | "rotatePerSecondVariance": 0,
41 | "maxRadius": 325,
42 | "maxRadiusVariance": 250,
43 | "minRadius": 0,
44 | "minRadiusVariance": 0,
45 | "radialAcceleration": 0,
46 | "radialAccelVariance": 0,
47 | "tangentialAcceleration": 0,
48 | "tangentialAccelVariance": 0,
49 | "renderBlendMode": 6,
50 | "textureBlendMode": 12,
51 | "emitterX": 600,
52 | "emitterY": 300,
53 | "configName": "galaxy",
54 | "textureFileName": "texture.png"
55 | }
--------------------------------------------------------------------------------
/example/assets/meteor.json:
--------------------------------------------------------------------------------
1 | {
2 | "configName": "meteor",
3 | "textureFileName": "texture.png",
4 | "emitterType": 0,
5 | "renderBlendMode": 6,
6 | "textureBlendMode": 12,
7 | "particleLifespan": 1.1,
8 | "particleLifespanVariance": 0.4,
9 | "startTime": 0.0,
10 | "duration": -1,
11 | "maxParticles": 289,
12 | "sourcePositionVariancex": 15,
13 | "sourcePositionVariancey": -20,
14 | "startParticleSize": 111,
15 | "startParticleSizeVariance": 10,
16 | "finishParticleSize": 40,
17 | "finishParticleSizeVariance": 0,
18 | "speed": 244,
19 | "speedVariance": 5,
20 | "emitterX": 380,
21 | "emitterY": 280,
22 | "gravityx": -1000,
23 | "gravityy": -740,
24 | "minRadius": 111,
25 | "minRadiusVariance": 0,
26 | "maxRadius": 222,
27 | "maxRadiusVariance": 0,
28 | "angle": -146,
29 | "angleVariance": 50,
30 | "rotatePerSecond": 222,
31 | "rotatePerSecondVariance": 0,
32 | "rotationStart": 720,
33 | "rotationStartVariance": 0,
34 | "rotationEnd": 0,
35 | "rotationEndVariance": 0,
36 | "radialAcceleration": 0,
37 | "radialAccelVariance": 0,
38 | "tangentialAcceleration": 0,
39 | "tangentialAccelVariance": 0,
40 | "startColorAlpha": 1,
41 | "startColorRed": 0.2,
42 | "startColorGreen": 0.4,
43 | "startColorBlue": 0.69,
44 | "startColorVarianceAlpha": 0.1,
45 | "startColorVarianceRed": 0,
46 | "startColorVarianceGreen": 0,
47 | "startColorVarianceBlue": 0.2,
48 | "finishColorAlpha": 1,
49 | "finishColorRed": 0,
50 | "finishColorGreen": 0,
51 | "finishColorBlue": 0,
52 | "finishColorVarianceAlpha": 0,
53 | "finishColorVarianceRed": 0,
54 | "finishColorVarianceGreen": 0,
55 | "finishColorVarianceBlue": 0
56 | }
--------------------------------------------------------------------------------
/example/assets/snow.json:
--------------------------------------------------------------------------------
1 | {
2 | "configName": "snow",
3 | "textureFileName": "texture.png",
4 | "emitterType": 0,
5 | "renderBlendMode": 6,
6 | "textureBlendMode": 12,
7 | "particleLifespan": 10,
8 | "particleLifespanVariance": 0,
9 | "startTime": 0,
10 | "duration": -1,
11 | "maxParticles": 500,
12 | "sourcePositionVariancex": 1000,
13 | "sourcePositionVariancey": 20,
14 | "startParticleSize": 0,
15 | "startParticleSizeVariance": 5,
16 | "finishParticleSize": 8,
17 | "finishParticleSizeVariance": 10,
18 | "speed": 0,
19 | "speedVariance": 374,
20 | "emitterX": 900,
21 | "emitterY": 50,
22 | "gravityx": 50,
23 | "gravityy": 50,
24 | "minRadius": 0,
25 | "minRadiusVariance": 0,
26 | "maxRadius": 0,
27 | "maxRadiusVariance": 0,
28 | "angle": 90,
29 | "angleVariance": 20,
30 | "rotatePerSecond": 0,
31 | "rotatePerSecondVariance": 0,
32 | "rotationStart": 0,
33 | "rotationStartVariance": 0,
34 | "rotationEnd": 0,
35 | "rotationEndVariance": 0,
36 | "radialAcceleration": 0,
37 | "radialAccelVariance": 0,
38 | "tangentialAcceleration": 0,
39 | "tangentialAccelVariance": 0,
40 | "startColorAlpha": 0.5,
41 | "startColorRed": 1,
42 | "startColorGreen": 1,
43 | "startColorBlue": 1,
44 | "startColorVarianceAlpha": 0.15,
45 | "startColorVarianceRed": 0,
46 | "startColorVarianceGreen": 0,
47 | "startColorVarianceBlue": 0,
48 | "finishColorAlpha": 1,
49 | "finishColorRed": 0,
50 | "finishColorGreen": 0,
51 | "finishColorBlue": 0,
52 | "finishColorVarianceAlpha": 0.2,
53 | "finishColorVarianceRed": 0,
54 | "finishColorVarianceGreen": 0,
55 | "finishColorVarianceBlue": 0
56 | }
--------------------------------------------------------------------------------
/example/assets/texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/example/assets/texture.png
--------------------------------------------------------------------------------
/example/integration_test/plugin_integration_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter integration test.
2 | //
3 | // Since integration tests run in a full Flutter application, they can interact
4 | // with the host side of a plugin implementation, unlike Dart unit tests.
5 | //
6 | // For more information about Flutter integration tests, please see
7 | // https://docs.flutter.dev/cookbook/testing/integration/introduction
8 |
9 | import 'package:flutter_test/flutter_test.dart';
10 | import 'package:integration_test/integration_test.dart';
11 |
12 | void main() {
13 | IntegrationTestWidgetsFlutterBinding.ensureInitialized();
14 |
15 | testWidgets('initial test', (WidgetTester tester) async {
16 | const String version = "42"; //await plugin.getPlatformVersion();
17 | // The version string depends on the host platform running the test, so
18 | // just assert that some non-empty string is returned.
19 | expect(version.isNotEmpty, true);
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:ui' as ui;
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/services.dart';
6 | import 'package:particular/particular.dart';
7 |
8 | void main() {
9 | runApp(const MyApp());
10 | }
11 |
12 | class MyApp extends StatefulWidget {
13 | const MyApp({super.key});
14 |
15 | @override
16 | State createState() => _MyAppState();
17 | }
18 |
19 | class _MyAppState extends State {
20 | // Add controller to change particle
21 | final _particleController = ParticularController();
22 |
23 | @override
24 | void initState() {
25 | _createParticles();
26 | super.initState();
27 | }
28 |
29 | // Load configs and texture of particle
30 | Future _createParticles() async {
31 |
32 | // Load particle configs file
33 | final fireworkJson = await rootBundle.loadString("assets/firework.json");
34 | final fireworkConfigs = jsonDecode(fireworkJson);
35 | _particleController.addConfigLayer(configsData: fireworkConfigs);
36 |
37 | // Or add (Flame) particle layer programmatically
38 | final bytes = await rootBundle.load("assets/texture.png");
39 | ui.Image? flameTexture = await loadUIImage(bytes.buffer.asUint8List());
40 |
41 | final flameConfigs = ParticularConfigs.initialize();
42 | flameConfigs.update(
43 | startSize: 100,
44 | startSizeVariance: 20,
45 | finishSize: 40,
46 | emitterX: 200,
47 | emitterY: 500,
48 | gravityY: -800,
49 | speed: 200,
50 | speedVariance: 5,
51 | sourcePositionVarianceX: 60,
52 | startColor: ARGB(1, 1, 0.2, 0),
53 | renderBlendMode: ui.BlendMode.dstIn,
54 | textureBlendMode: ui.BlendMode.plus,
55 | );
56 |
57 | final layer = ParticularLayer(texture: flameTexture, configs: flameConfigs);
58 | _particleController.addParticularLayer(layer);
59 | }
60 |
61 | @override
62 | Widget build(BuildContext context) {
63 | return MaterialApp(
64 | home: Scaffold(
65 | backgroundColor: Colors.black,
66 | body: Particular(
67 | controller: _particleController,
68 | ),
69 | ),
70 | );
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/macos/Flutter/GeneratedPluginRegistrant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | import FlutterMacOS
6 | import Foundation
7 |
8 |
9 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
10 | }
11 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/.app_filename:
--------------------------------------------------------------------------------
1 | example.app
2 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig:
--------------------------------------------------------------------------------
1 | // This is a generated file; do not edit or check into version control.
2 | FLUTTER_ROOT=/Users/neo/Dev/flutter
3 | FLUTTER_APPLICATION_PATH=/Users/neo/IdeaProjects/particular/example
4 | COCOAPODS_PARALLEL_CODE_SIGN=true
5 | FLUTTER_BUILD_DIR=build
6 | FLUTTER_BUILD_NAME=1.0.0
7 | FLUTTER_BUILD_NUMBER=1
8 | DART_OBFUSCATION=false
9 | TRACK_WIDGET_CREATION=true
10 | TREE_SHAKE_ICONS=false
11 | PACKAGE_CONFIG=.dart_tool/package_config.json
12 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/FlutterMacOS.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # This podspec is NOT to be published. It is only used as a local source!
3 | # This is a generated file; do not edit or check into version control.
4 | #
5 |
6 | Pod::Spec.new do |s|
7 | s.name = 'FlutterMacOS'
8 | s.version = '1.0.0'
9 | s.summary = 'A UI toolkit for beautiful and fast apps.'
10 | s.homepage = 'https://flutter.dev'
11 | s.license = { :type => 'BSD' }
12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
14 | s.osx.deployment_target = '10.14'
15 | # Framework linking is handled by Flutter tooling, not CocoaPods.
16 | # Add a placeholder to satisfy `s.dependency 'FlutterMacOS'` plugin podspecs.
17 | s.vendored_frameworks = 'path/to/nothing'
18 | end
19 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/FlutterOutputs.xcfilelist:
--------------------------------------------------------------------------------
1 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/App
2 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/Info.plist
3 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/AssetManifest.bin
4 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/AssetManifest.json
5 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/FontManifest.json
6 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/NOTICES.Z
7 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/assets/firework.json
8 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/assets/galaxy.json
9 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/assets/particle.json
10 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/assets/snow.json
11 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/assets/texture.png
12 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/fonts/MaterialIcons-Regular.otf
13 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/isolate_snapshot_data
14 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/kernel_blob.bin
15 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/shaders/ink_sparkle.frag
16 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/App.framework/Versions/A/Resources/flutter_assets/vm_snapshot_data
17 | /Users/mansourdjawadi/development/particle/particular/example/build/macos/Build/Products/Debug/FlutterMacOS.framework/Versions/A/FlutterMacOS
18 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/flutter_export_environment.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This is a generated file; do not edit or check into version control.
3 | export "FLUTTER_ROOT=/Users/neo/Dev/flutter"
4 | export "FLUTTER_APPLICATION_PATH=/Users/neo/IdeaProjects/particular/example"
5 | export "COCOAPODS_PARALLEL_CODE_SIGN=true"
6 | export "FLUTTER_BUILD_DIR=build"
7 | export "FLUTTER_BUILD_NAME=1.0.0"
8 | export "FLUTTER_BUILD_NUMBER=1"
9 | export "DART_OBFUSCATION=false"
10 | export "TRACK_WIDGET_CREATION=true"
11 | export "TREE_SHAKE_ICONS=false"
12 | export "PACKAGE_CONFIG=.dart_tool/package_config.json"
13 |
--------------------------------------------------------------------------------
/example/macos/Flutter/ephemeral/tripwire:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/example/macos/Flutter/ephemeral/tripwire
--------------------------------------------------------------------------------
/example/macos/Podfile:
--------------------------------------------------------------------------------
1 | platform :osx, '10.14'
2 |
3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
5 |
6 | project 'Runner', {
7 | 'Debug' => :debug,
8 | 'Profile' => :release,
9 | 'Release' => :release,
10 | }
11 |
12 | def flutter_root
13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
14 | unless File.exist?(generated_xcode_build_settings_path)
15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
16 | end
17 |
18 | File.foreach(generated_xcode_build_settings_path) do |line|
19 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
20 | return matches[1].strip if matches
21 | end
22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
23 | end
24 |
25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
26 |
27 | flutter_macos_podfile_setup
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 | use_modular_headers!
32 |
33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
34 | target 'RunnerTests' do
35 | inherit! :search_paths
36 | end
37 | end
38 |
39 | post_install do |installer|
40 | installer.pods_project.targets.each do |target|
41 | flutter_additional_macos_build_settings(target)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/example/macos/Pods/Local Podspecs/FlutterMacOS.podspec.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FlutterMacOS",
3 | "version": "1.0.0",
4 | "summary": "A UI toolkit for beautiful and fast apps.",
5 | "homepage": "https://flutter.dev",
6 | "license": {
7 | "type": "BSD"
8 | },
9 | "authors": {
10 | "Flutter Dev Team": "flutter-dev@googlegroups.com"
11 | },
12 | "source": {
13 | "git": "https://github.com/flutter/engine",
14 | "tag": "1.0.0"
15 | },
16 | "platforms": {
17 | "osx": "10.14"
18 | },
19 | "vendored_frameworks": "path/to/nothing"
20 | }
21 |
--------------------------------------------------------------------------------
/example/macos/Pods/Pods.xcodeproj/xcuserdata/mansourdjawadi.xcuserdatad/xcschemes/FlutterMacOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/macos/Pods/Pods.xcodeproj/xcuserdata/mansourdjawadi.xcuserdatad/xcschemes/Pods-Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/macos/Pods/Pods.xcodeproj/xcuserdata/mansourdjawadi.xcuserdatad/xcschemes/Pods-RunnerTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/macos/Pods/Pods.xcodeproj/xcuserdata/mansourdjawadi.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | FlutterMacOS.xcscheme
8 |
9 | isShown
10 |
11 |
12 | Pods-Runner.xcscheme
13 |
14 | isShown
15 |
16 |
17 | Pods-RunnerTests.xcscheme
18 |
19 | isShown
20 |
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/FlutterMacOS/FlutterMacOS.debug.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | CODE_SIGN_IDENTITY =
3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FlutterMacOS
4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
5 | PODS_BUILD_DIR = ${BUILD_DIR}
6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
7 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
8 | PODS_ROOT = ${SRCROOT}
9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral
10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
12 | SKIP_INSTALL = YES
13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
14 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/FlutterMacOS/FlutterMacOS.release.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | CODE_SIGN_IDENTITY =
3 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FlutterMacOS
4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
5 | PODS_BUILD_DIR = ${BUILD_DIR}
6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
7 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
8 | PODS_ROOT = ${SRCROOT}
9 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../Flutter/ephemeral
10 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
11 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
12 | SKIP_INSTALL = YES
13 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
14 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | ${PODS_DEVELOPMENT_LANGUAGE}
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | ${PRODUCT_BUNDLE_IDENTIFIER}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | ${CURRENT_PROJECT_VERSION}
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-acknowledgements.markdown:
--------------------------------------------------------------------------------
1 | # Acknowledgements
2 | This application makes use of the following third party libraries:
3 | Generated by CocoaPods - https://cocoapods.org
4 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-acknowledgements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | FooterText
9 | This application makes use of the following third party libraries:
10 | Title
11 | Acknowledgements
12 | Type
13 | PSGroupSpecifier
14 |
15 |
16 | FooterText
17 | Generated by CocoaPods - https://cocoapods.org
18 | Title
19 |
20 | Type
21 | PSGroupSpecifier
22 |
23 |
24 | StringsTable
25 | Acknowledgements
26 | Title
27 | Acknowledgements
28 |
29 |
30 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-dummy.m:
--------------------------------------------------------------------------------
1 | #import
2 | @interface PodsDummy_Pods_Runner : NSObject
3 | @end
4 | @implementation PodsDummy_Pods_Runner
5 | @end
6 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner-umbrella.h:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
14 | FOUNDATION_EXPORT double Pods_RunnerVersionNumber;
15 | FOUNDATION_EXPORT const unsigned char Pods_RunnerVersionString[];
16 |
17 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks'
4 | PODS_BUILD_DIR = ${BUILD_DIR}
5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
6 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
7 | PODS_ROOT = ${SRCROOT}/Pods
8 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
10 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.modulemap:
--------------------------------------------------------------------------------
1 | framework module Pods_Runner {
2 | umbrella header "Pods-Runner-umbrella.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks'
4 | PODS_BUILD_DIR = ${BUILD_DIR}
5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
6 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
7 | PODS_ROOT = ${SRCROOT}/Pods
8 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
10 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks'
4 | PODS_BUILD_DIR = ${BUILD_DIR}
5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
6 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
7 | PODS_ROOT = ${SRCROOT}/Pods
8 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
10 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | ${PODS_DEVELOPMENT_LANGUAGE}
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | ${PRODUCT_BUNDLE_IDENTIFIER}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | ${CURRENT_PROJECT_VERSION}
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-acknowledgements.markdown:
--------------------------------------------------------------------------------
1 | # Acknowledgements
2 | This application makes use of the following third party libraries:
3 | Generated by CocoaPods - https://cocoapods.org
4 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-acknowledgements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreferenceSpecifiers
6 |
7 |
8 | FooterText
9 | This application makes use of the following third party libraries:
10 | Title
11 | Acknowledgements
12 | Type
13 | PSGroupSpecifier
14 |
15 |
16 | FooterText
17 | Generated by CocoaPods - https://cocoapods.org
18 | Title
19 |
20 | Type
21 | PSGroupSpecifier
22 |
23 |
24 | StringsTable
25 | Acknowledgements
26 | Title
27 | Acknowledgements
28 |
29 |
30 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-dummy.m:
--------------------------------------------------------------------------------
1 | #import
2 | @interface PodsDummy_Pods_RunnerTests : NSObject
3 | @end
4 | @implementation PodsDummy_Pods_RunnerTests
5 | @end
6 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests-umbrella.h:
--------------------------------------------------------------------------------
1 | #ifdef __OBJC__
2 | #import
3 | #else
4 | #ifndef FOUNDATION_EXPORT
5 | #if defined(__cplusplus)
6 | #define FOUNDATION_EXPORT extern "C"
7 | #else
8 | #define FOUNDATION_EXPORT extern
9 | #endif
10 | #endif
11 | #endif
12 |
13 |
14 | FOUNDATION_EXPORT double Pods_RunnerTestsVersionNumber;
15 | FOUNDATION_EXPORT const unsigned char Pods_RunnerTestsVersionString[];
16 |
17 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | PODS_BUILD_DIR = ${BUILD_DIR}
4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
6 | PODS_ROOT = ${SRCROOT}/Pods
7 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
8 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
9 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.modulemap:
--------------------------------------------------------------------------------
1 | framework module Pods_RunnerTests {
2 | umbrella header "Pods-RunnerTests-umbrella.h"
3 |
4 | export *
5 | module * { export * }
6 | }
7 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | PODS_BUILD_DIR = ${BUILD_DIR}
4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
6 | PODS_ROOT = ${SRCROOT}/Pods
7 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
8 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
9 |
--------------------------------------------------------------------------------
/example/macos/Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig:
--------------------------------------------------------------------------------
1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
3 | PODS_BUILD_DIR = ${BUILD_DIR}
4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
6 | PODS_ROOT = ${SRCROOT}/Pods
7 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
8 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
9 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: particular_example
2 | description: "Demonstrates how to use the particular package."
3 | publish_to: 'none'
4 |
5 | environment:
6 | sdk: '>=3.3.1 <4.0.0'
7 | dependencies:
8 | flutter:
9 | sdk: flutter
10 | image:
11 | particular:
12 | path: ../
13 |
14 | dev_dependencies:
15 | integration_test:
16 | sdk: flutter
17 | flutter_test:
18 | sdk: flutter
19 | flutter_lints: ^5.0.0
20 |
21 | flutter:
22 | uses-material-design: true
23 | assets:
24 | - assets/
--------------------------------------------------------------------------------
/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 in the flutter_test package. 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:particular_example/main.dart';
12 |
13 | void main() {
14 | testWidgets('Verify Platform version', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(const MyApp());
17 |
18 | // Verify that platform version is retrieved.
19 | expect(
20 | find.byWidgetPredicate(
21 | (Widget widget) =>
22 | widget is Text && widget.data!.startsWith('Running on:'),
23 | ),
24 | findsOneWidget,
25 | );
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/lib/particular.dart:
--------------------------------------------------------------------------------
1 | ///MIT License
2 |
3 | ///Copyright (c) 2024 Mansour Djawadi
4 |
5 | /// Permission is hereby granted, free of charge, to any person obtaining a copy
6 | /// of this software and associated documentation files (the "Software"), to deal
7 | /// in the Software without restriction, including without limitation the rights
8 | /// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | /// copies of the Software, and to permit persons to whom the Software is
10 | /// furnished to do so, subject to the following conditions:
11 |
12 | library;
13 |
14 | export 'src/blending.dart';
15 | export 'src/image_loader.dart';
16 | export 'src/particle.dart';
17 | export 'src/particular_configs.dart';
18 | export 'src/particular_controller.dart';
19 | export 'src/particular_emitter.dart';
20 | export 'src/particular_layer.dart';
21 |
--------------------------------------------------------------------------------
/lib/src/blending.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | /// kClear_Mode, //!< [0, 0]
4 | /// kSrc_Mode, //!< [Sa, Sc]
5 | /// kDst_Mode, //!< [Da, Dc]
6 | /// kSrcOver_Mode, //!< [Sa + Da - Sa*Da, Rc = Sc + (1 - Sa)*Dc]
7 | /// kDstOver_Mode, //!< [Sa + Da - Sa*Da, Rc = Dc + (1 - Da)*Sc]
8 | /// kSrcIn_Mode, //!< [Sa * Da, Sc * Da]
9 | /// kDstIn_Mode, //!< [Sa * Da, Sa * Dc]
10 | /// kSrcOut_Mode, //!< [Sa * (1 - Da), Sc * (1 - Da)]
11 | /// kDstOut_Mode, //!< [Da * (1 - Sa), Dc * (1 - Sa)]
12 | /// kSrcATop_Mode, //!< [Da, Sc * Da + (1 - Sa) * Dc]
13 | /// kDstATop_Mode, //!< [Sa, Sa * Dc + Sc * (1 - Da)]
14 | /// kXor_Mode, //!< [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
15 | /// kPlus_Mode, //!< [Sa + Da, Sc + Dc]
16 | /// kModulate_Mode, // multiplies all components (= alpha and color)
17 | List blendModeList = [
18 | BlendModeItem(
19 | BlendMode.clear,
20 | BlendFunction.zero,
21 | BlendFunction.zero,
22 | ),
23 | BlendModeItem(
24 | BlendMode.color,
25 | BlendFunction.zero,
26 | BlendFunction.sourceColor,
27 | ),
28 | BlendModeItem(
29 | BlendMode.colorBurn,
30 | BlendFunction.zero,
31 | BlendFunction.oneMinusSourceColor,
32 | ),
33 | // BlendModeItem(
34 | // BlendMode.colorDodge,
35 | // BlendFunction.zero,
36 | // BlendFunction.one,
37 | // ),
38 | BlendModeItem(
39 | BlendMode.darken,
40 | BlendFunction.oneMinusDestinationColor,
41 | BlendFunction.oneMinusSourceColor,
42 | ),
43 | BlendModeItem(
44 | BlendMode.difference,
45 | BlendFunction.zero,
46 | BlendFunction.oneMinusSourceColor,
47 | ),
48 | BlendModeItem(
49 | BlendMode.dst,
50 | BlendFunction.zero,
51 | BlendFunction.one,
52 | ),
53 | BlendModeItem(
54 | BlendMode.dstATop,
55 | BlendFunction.destinationAlpha,
56 | BlendFunction.oneMinusSourceAlpha,
57 | ),
58 | BlendModeItem(
59 | BlendMode.dstIn,
60 | BlendFunction.zero,
61 | BlendFunction.sourceAlpha,
62 | ),
63 | // Erase
64 | BlendModeItem(
65 | BlendMode.dstOut,
66 | BlendFunction.zero,
67 | BlendFunction.oneMinusSourceAlpha,
68 | ),
69 | BlendModeItem(
70 | BlendMode.dstOver,
71 | BlendFunction.oneMinusDestinationAlpha,
72 | BlendFunction.one,
73 | ),
74 | BlendModeItem(
75 | BlendMode.exclusion,
76 | BlendFunction.oneMinusDestinationColor,
77 | BlendFunction.oneMinusSourceColor,
78 | ),
79 | BlendModeItem(
80 | BlendMode.hardLight,
81 | BlendFunction.zero,
82 | BlendFunction.oneMinusSourceColor,
83 | ),
84 | BlendModeItem(
85 | BlendMode.hue,
86 | BlendFunction.oneMinusDestinationColor,
87 | BlendFunction.zero,
88 | ),
89 | // BlendModeItem(
90 | // BlendMode.lighten,
91 | // BlendFunction.oneMinusDestinationColor,
92 | // BlendFunction.one,
93 | // ),
94 | BlendModeItem(
95 | BlendMode.luminosity,
96 | BlendFunction.zero,
97 | BlendFunction.oneMinusSourceColor,
98 | ),
99 | // BlendModeItem(
100 | // BlendMode.modulate,
101 | // BlendFunction.one,
102 | // BlendFunction.oneMinusSourceColor,
103 | // ),
104 | // Multiply
105 | BlendModeItem(
106 | BlendMode.multiply,
107 | BlendFunction.destinationColor,
108 | BlendFunction.oneMinusSourceAlpha,
109 | ),
110 | BlendModeItem(
111 | BlendMode.overlay,
112 | BlendFunction.oneMinusDestinationColor,
113 | BlendFunction.oneMinusSourceColor,
114 | ),
115 | // Add
116 | BlendModeItem(
117 | BlendMode.plus,
118 | BlendFunction.one,
119 | BlendFunction.one,
120 | ),
121 | // Screen
122 | BlendModeItem(
123 | BlendMode.screen,
124 | BlendFunction.one,
125 | BlendFunction.oneMinusSourceColor,
126 | ),
127 | BlendModeItem(
128 | BlendMode.softLight,
129 | BlendFunction.zero,
130 | BlendFunction.oneMinusSourceColor,
131 | ),
132 | BlendModeItem(
133 | BlendMode.src,
134 | BlendFunction.one,
135 | BlendFunction.zero,
136 | ),
137 | BlendModeItem(
138 | BlendMode.srcATop,
139 | BlendFunction.destinationAlpha,
140 | BlendFunction.oneMinusSourceAlpha,
141 | ),
142 | BlendModeItem(
143 | BlendMode.srcIn,
144 | BlendFunction.destinationAlpha,
145 | BlendFunction.zero,
146 | ),
147 | BlendModeItem(
148 | BlendMode.srcOut,
149 | BlendFunction.oneMinusDestinationAlpha,
150 | BlendFunction.zero,
151 | ),
152 | // Normal
153 | BlendModeItem(
154 | BlendMode.srcOver,
155 | BlendFunction.one,
156 | BlendFunction.oneMinusSourceAlpha,
157 | ),
158 | BlendModeItem(
159 | BlendMode.xor,
160 | BlendFunction.oneMinusDestinationColor,
161 | BlendFunction.oneMinusSourceColor,
162 | ),
163 | ];
164 |
165 | class BlendModeItem {
166 | /// The blend mode to apply.
167 | final BlendMode blendMode;
168 |
169 | /// The source blend function to apply.
170 | ///
171 | /// Defaults to [BlendFunction.zero].
172 | final BlendFunction sourceBlendFunction;
173 |
174 | /// The destination blend function to apply.
175 | ///
176 | /// Defaults to [BlendFunction.zero].
177 | final BlendFunction destinationBlendFunction;
178 |
179 | /// Creates a new blend mode item.
180 | ///
181 | /// If [sourceBlendFunction] and [destinationBlendFunction] are not provided,
182 | /// they default to [BlendFunction.zero] and [BlendFunction.zero] respectively.
183 | BlendModeItem(
184 | this.blendMode, [
185 | this.sourceBlendFunction = BlendFunction.zero,
186 | this.destinationBlendFunction = BlendFunction.zero,
187 | ]);
188 |
189 | /// Gets the blend mode for particle rendering.
190 | static BlendMode computeBlendMode(BlendFunction src, BlendFunction dst) {
191 | if (dst == BlendFunction.zero) return BlendMode.clear;
192 | if (src == BlendFunction.zero) {
193 | return switch (dst) {
194 | BlendFunction.oneMinusSourceColor => BlendMode.screen, //erase
195 | BlendFunction.sourceAlpha => BlendMode.srcIn, //mask
196 | _ => BlendMode.srcOver,
197 | };
198 | }
199 | if (src == BlendFunction.one) {
200 | return switch (dst) {
201 | BlendFunction.one => BlendMode.plus,
202 | BlendFunction.oneMinusSourceColor => BlendMode.screen,
203 | _ => BlendMode.srcOver,
204 | };
205 | }
206 | if (src == BlendFunction.destinationColor &&
207 | dst == BlendFunction.oneMinusSourceAlpha) {
208 | return BlendMode.multiply;
209 | }
210 | if (src == BlendFunction.oneMinusDestinationAlpha &&
211 | dst == BlendFunction.destinationAlpha) {
212 | return BlendMode.dst;
213 | }
214 |
215 | // "none":,ONE, ZERO
216 | // "normal": ONE, ONE_MINUS_SOURCE_ALPHA
217 | // "add": ONE, ONE
218 | // "screen": ONE, ONE_MINUS_SOURCE_COLOR
219 | // "erase": ZERO, ONE_MINUS_SOURCE_ALPHA
220 | // "mask": ZERO, SOURCE_ALPHA
221 | // "multiply": DESTINATION_COLOR, ONE_MINUS_SOURCE_ALPHA
222 | // "below": ONE_MINUS_DESTINATION_ALPHA, DESTINATION_ALPHA
223 | return BlendMode.srcOver;
224 | }
225 |
226 | // Computes the blend mode based on the source and destination blend functions.
227 | static BlendMode computeFlutterBlendMode(
228 | BlendFunction src, BlendFunction dst) {
229 | var item = blendModeList.firstWhere(
230 | (b) =>
231 | b.sourceBlendFunction == src && b.destinationBlendFunction == dst,
232 | orElse: () {
233 | return BlendModeItem(BlendMode.plus);
234 | });
235 | return item.blendMode;
236 | }
237 | }
238 |
239 | /// Enum representing different blend functions used for particle rendering.
240 | enum BlendFunction {
241 | zero(0), // GL_ZERO
242 | one(1), // GL_ONE
243 | // color(10), // GL_COLOR
244 | sourceColor(0x300), // GL_SOURCE_COLOR
245 | oneMinusSourceColor(0x301), // GL_ONE_MINUS_SOURCE_COLOR
246 | sourceAlpha(0x302), // GL_SOURCE_ALPHA
247 | oneMinusSourceAlpha(0x303), // GL_ONE_MINUS_SOURCE_ALPHA
248 | destinationAlpha(0x304), // GL_DESTINATION_ALPHA
249 | oneMinusDestinationAlpha(0x305), // GL_ONE_MINUS_DESTINATION_ALPHA
250 | destinationColor(0x306), // GL_DESTINATION_COLOR
251 | oneMinusDestinationColor(0x307), // GL_ONE_MINUS_DESTINATION_COLOR
252 | sourceAlphaSaturate(0x307); // GL_SOURCE_ALPHA_SATURATE
253 |
254 | /// Converts the given [value] to a [BlendFunction].
255 | final int value;
256 |
257 | /// The constructor of the blend function.
258 | const BlendFunction(this.value);
259 |
260 | /// Converts the given [value] to a [BlendFunction].
261 | ///
262 | /// The [value] parameter is the integer representation of the [BlendFunction].
263 | ///
264 | /// Returns the corresponding [BlendFunction] object.
265 | static BlendFunction fromValue(int value) {
266 | return values.where((item) => item.value == value).first;
267 | }
268 |
269 | /// Returns a string representation of the object. In this case, it returns the value of the 'name' property.
270 | @override
271 | String toString() => name;
272 | }
273 |
--------------------------------------------------------------------------------
/lib/src/image_loader.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:ui' as ui;
3 | import 'package:flutter/services.dart';
4 |
5 | /// Loads an image from the given byte array and returns it as a `ui.Image`.
6 | ///
7 | /// The [bytes] parameter is the byte array containing the image data.
8 | ///
9 | /// Returns a `Future` that completes with the loaded image.
10 | Future loadUIImage(Uint8List bytes) async {
11 | final Completer completer = Completer();
12 | ui.decodeImageFromList(bytes, (ui.Image img) {
13 | completer.complete(img);
14 | });
15 | return completer.future;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/src/particle.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: overridden_fields
2 |
3 | import 'dart:math' as math;
4 |
5 | import 'package:flutter/material.dart';
6 |
7 | /// Two type of emitters (liner of radial)
8 | enum EmitterType {
9 | gravity,
10 | radius;
11 |
12 | /// Returns a string representation of the object. In this case, it returns the value of the 'name' property.
13 | @override
14 | String toString() => name;
15 | }
16 |
17 | /// Represents a particle in the particle system.
18 | class Particle {
19 | /// Type of emitter (linear or radial).
20 | EmitterType emitterType = EmitterType.gravity;
21 |
22 | /// Time since particle instantiation.
23 | int age = 0;
24 |
25 | /// Initial speed of the particle.
26 | double speed = 0;
27 |
28 | /// Lifespan of the particle.
29 | int lifespan = 0;
30 |
31 | /// Size of the particle.
32 | double size = 100;
33 |
34 | /// Dynamic X position of the particle.
35 | double x = 0;
36 |
37 | /// Dynamic Y position of the particle.
38 | double y = 0;
39 |
40 | /// Dynamic angle of the particle.
41 | double angle = 0;
42 |
43 | /// Dynamic rotation of the particle.
44 | double rotation = 0;
45 |
46 | /// Dynamic radius of the particle.
47 | double radius = 0;
48 |
49 | /// Dynamic change in radius of the particle.
50 | double radiusDelta = 0;
51 |
52 | /// Dynamic X position of the emitter.
53 | double emitterX = 0;
54 |
55 | /// Dynamic Y position of the emitter.
56 | double emitterY = 0;
57 |
58 | /// Dynamic X velocity of the particle.
59 | double velocityX = 0;
60 |
61 | /// Dynamic Y velocity of the particle.
62 | double velocityY = 0;
63 |
64 | /// Dynamic X gravity of the particle.
65 | double gravityX = 0;
66 |
67 | /// Dynamic Y gravity of the particle.
68 | double gravityY = 0;
69 |
70 | /// Initial size of the particle.
71 | double startSize = 0;
72 |
73 | /// Final size of the particle.
74 | double finishSize = 0;
75 |
76 | /// Minimum radius of the particle.
77 | double minRadius = 0;
78 |
79 | /// Maximum radius of the particle.
80 | double maxRadius = 0;
81 |
82 | /// Rotation per second of the particle.
83 | double rotatePerSecond = 0;
84 |
85 | /// Start rotation of the particle.
86 | double startRotation = 0;
87 |
88 | /// End rotation of the particle.
89 | double finishRotation = 0;
90 |
91 | /// Radial acceleration of the particle.
92 | double radialAcceleration = 0;
93 |
94 | /// Tangential acceleration of the particle.
95 | double tangentialAcceleration = 0;
96 |
97 | /// Color of the particle.
98 | ParticleColor color = ParticleColor(0);
99 |
100 | /// Transform of the particle.
101 | ParticleTransform transform = ParticleTransform(0, 0, 0, 0);
102 |
103 | /// Initial color of the particle.
104 | Color startColor = Colors.white;
105 |
106 | /// Final color of the particle.
107 | Color finishColor = Colors.white;
108 |
109 | /// Whether the particle is alive or not.
110 | bool isAlive = false;
111 |
112 | /// Initializes particle properties.
113 | void initialize({
114 | EmitterType emitterType = EmitterType.gravity,
115 | required int age,
116 | required int lifespan,
117 | required double speed,
118 | required double emitterX,
119 | required double emitterY,
120 | required double startSize,
121 | required double finishSize,
122 | required Color startColor,
123 | required Color finishColor,
124 | required double angle,
125 | required double rotatePerSecond,
126 | required double startRotation,
127 | required double finishRotation,
128 | required double radialAcceleration,
129 | required double tangentialAcceleration,
130 | required double minRadius,
131 | required double maxRadius,
132 | required double gravityX,
133 | required double gravityY,
134 | }) {
135 | this.emitterType = emitterType;
136 | this.age = age;
137 | this.speed = speed;
138 | this.lifespan = lifespan;
139 | this.emitterX = emitterX;
140 | this.emitterY = emitterY;
141 | this.startSize = startSize;
142 | this.finishSize = finishSize;
143 | this.startColor = startColor;
144 | this.finishColor = finishColor;
145 | this.angle = angle / 180.0 * math.pi;
146 | this.rotatePerSecond = rotatePerSecond / 180.0 * math.pi;
147 | this.startRotation = startRotation / 180.0 * math.pi;
148 | this.finishRotation = finishRotation / 180.0 * math.pi;
149 | this.radialAcceleration = radialAcceleration;
150 | this.tangentialAcceleration = tangentialAcceleration;
151 | this.minRadius = minRadius;
152 | this.maxRadius = maxRadius;
153 | this.gravityX = gravityX;
154 | this.gravityY = gravityY;
155 |
156 | x = emitterX;
157 | y = emitterY;
158 | size = startSize;
159 | rotation = this.startRotation;
160 | color.update(startColor.a, startColor.r, startColor.g, startColor.b);
161 | radius = maxRadius;
162 | radiusDelta = (minRadius - maxRadius);
163 | velocityX = speed * math.cos(this.angle);
164 | velocityY = speed * math.sin(this.angle);
165 | isAlive = true;
166 | }
167 |
168 | /// Updates the particle's state based on the given [deltaTime].
169 | ///
170 | /// If the particle has already reached its lifespan, it will return without
171 | /// performing any updates.
172 | void update(int deltaTime) {
173 | // If the particle has reached its lifespan, return without updating.
174 | if (!isAlive) return;
175 | age += deltaTime;
176 | final ratio = age / lifespan;
177 | final rate = deltaTime / 1000;
178 |
179 | // Update the particle's state based on its emitter type.
180 | if (emitterType == EmitterType.radius) {
181 | angle -= rotatePerSecond * rate;
182 | radius += radiusDelta * rate;
183 |
184 | // Calculate the cosine and sine of the angle.
185 | final radiusCos = math.cos(angle);
186 | final radiusSin = math.sin(angle);
187 |
188 | // Calculate the new x and y coordinates of the particle.
189 | x = emitterX - radiusCos * radius;
190 | y = emitterY - radiusSin * radius;
191 | } else {
192 | // Calculate the distance between the particle and its emitter.
193 | final distanceX = x - emitterX;
194 | final distanceY = y - emitterY;
195 | final distanceScalar =
196 | math.sqrt(distanceX * distanceX + distanceY * distanceY);
197 |
198 | // Avoid division by zero.
199 | final distanceScalarClamp = distanceScalar < 0.01 ? 0.01 : distanceScalar;
200 |
201 | // Calculate the radial and tangential components of the distance.
202 | final radialX = distanceX / distanceScalarClamp;
203 | final radialY = distanceY / distanceScalarClamp;
204 |
205 | final radialXModified = radialX * radialAcceleration;
206 | final radialYModified = radialY * radialAcceleration;
207 |
208 | // Calculate the tangential components.
209 | final tangentialX = radialX;
210 | final tangentialY = radialY;
211 |
212 | final tangentialXModified = -tangentialY * tangentialAcceleration;
213 | final tangentialYModified = tangentialX * tangentialAcceleration;
214 |
215 | velocityX += rate * (gravityX + radialXModified + tangentialXModified);
216 | velocityY += rate * (gravityY + radialYModified + tangentialYModified);
217 |
218 | x += velocityX * rate;
219 | y += velocityY * rate;
220 | }
221 |
222 | color.lerp(startColor, finishColor, ratio);
223 | size = startSize + (finishSize - startSize) * ratio;
224 | rotation = startRotation + (finishRotation - startRotation) * ratio;
225 | }
226 |
227 | /// Return true if finished its life and reserved in pooling system
228 | bool isDyingTime() => age > lifespan;
229 | }
230 |
231 | /// Dedicated transform class for pooling system
232 | /// Help to reuse transforms after rendering
233 | /// A transform class for particles in pooling system.
234 | class ParticleTransform extends RSTransform {
235 | /// Scaled cosine of transform.
236 | double _scaledCos = 0;
237 |
238 | /// Scaled sine of transform.
239 | double _scaledSin = 0;
240 |
241 | /// Translate value in x
242 | double _tx = 0;
243 |
244 | /// Translate value in y
245 | double _ty = 0;
246 |
247 | /// Creates a new particle transform.
248 | ParticleTransform(super.scos, super.ssin, super.tx, super.ty);
249 |
250 | /// Update all transform specs
251 | void update({
252 | required double rotation,
253 | required double scale,
254 | required double anchorX,
255 | required double anchorY,
256 | required double translateX,
257 | required double translateY,
258 | }) {
259 | _scaledCos = math.cos(rotation) * scale;
260 | _scaledSin = math.sin(rotation) * scale;
261 | _tx = translateX - _scaledCos * anchorX + _scaledSin * anchorY;
262 | _ty = translateY - _scaledSin * anchorX - _scaledCos * anchorY;
263 | }
264 |
265 | /// The cosine of the rotation multiplied by the scale factor.
266 | @override
267 | double get scos => _scaledCos;
268 |
269 | /// The sine of the rotation multiplied by that same scale factor.
270 | @override
271 | double get ssin => _scaledSin;
272 |
273 | /// The x coordinate of the translation, minus [scos] multiplied by the
274 | /// x-coordinate of the rotation point, plus [ssin] multiplied by the
275 | /// y-coordinate of the rotation point.
276 | @override
277 | double get tx => _tx;
278 |
279 | /// The y coordinate of the translation, minus [ssin] multiplied by the
280 | /// x-coordinate of the rotation point, minus [scos] multiplied by the
281 | /// y-coordinate of the rotation point.
282 | @override
283 | double get ty => _ty;
284 | }
285 |
286 | /// Dedicated color class for pooling system
287 | /// Help to reuse colors after rendering
288 | class ParticleColor extends Color {
289 | @override
290 | int value = 0;
291 |
292 | ParticleColor(super.value);
293 |
294 | /// Updates the color channels separately.
295 | ///
296 | /// The parameters [a], [r], [g], and [b] represent the alpha, red, green,
297 | /// and blue color channels respectively. Each channel is expected to be a
298 | /// value between 0 and 255.
299 | ///
300 | /// The color value of this [ParticleColor] object is updated with the
301 | /// provided color channels.
302 | void update(double a, double r, double g, double b) {
303 | // Combine the color channels into a single integer value.
304 | value = ((((a * 255).round() & 0xff) <<
305 | 24) | // Shift alpha channel and bitwise AND with 0xff
306 | (((r * 255).round() & 0xff) <<
307 | 16) | // Shift red channel and bitwise AND with 0xff
308 | (((g * 255).round() & 0xff) <<
309 | 8) | // Shift green channel and bitwise AND with 0xff
310 | (((b * 255).round() & 0xff) <<
311 | 0)) & // Shift blue channel and bitwise AND with 0xff
312 | 0xFFFFFFFF; // Mask the result to 32 bits
313 | }
314 |
315 | /// Linearly interpolates the color channels of this [ParticleColor]
316 | /// instance between the provided [from] and [to] colors using the
317 | /// given interpolation [delta].
318 | ///
319 | /// The color channels are interpolated separately using the [_lerp]
320 | /// function. The resulting interpolated color channels are then used to
321 | /// update the color of this [ParticleColor] instance using the [update]
322 | /// method.
323 | ///
324 | /// Parameters:
325 | /// - from: The starting color.
326 | /// - to: The ending color.
327 | /// - delta: The interpolation factor.
328 | void lerp(Color from, Color to, double delta) {
329 | update(
330 | _lerp(from.a, to.a, delta),
331 | _lerp(from.r, to.r, delta),
332 | _lerp(from.g, to.g, delta),
333 | _lerp(from.b, to.b, delta),
334 | );
335 | }
336 |
337 | /// Linearly interpolates between two integers.
338 | ///
339 | /// The function takes in the starting integer [from], the ending integer
340 | /// [to], and the interpolation factor [delta]. It returns the interpolated
341 | /// value as a [double].
342 | ///
343 | /// Parameters:
344 | /// - from: The starting integer.
345 | /// - to: The ending integer.
346 | /// - delta: The interpolation factor.
347 | ///
348 | /// Returns:
349 | /// The interpolated value as a [double].
350 | double _lerp(double from, double to, double delta) =>
351 | (from + (to - from) * delta).clamp(0, 1);
352 |
353 | /// The alpha channel of this color in an 8 bit value.
354 | ///
355 | /// A value of 0 means this color is fully transparent. A value of 255 means
356 | /// this color is fully opaque.
357 | @override
358 | int get alpha => (0xff000000 & value) >> 24;
359 |
360 | /// The red channel of this color in an 8 bit value.
361 | @override
362 | int get red => (0x00ff0000 & value) >> 16;
363 |
364 | /// The green channel of this color in an 8 bit value.
365 | @override
366 | int get green => (0x0000ff00 & value) >> 8;
367 |
368 | /// The blue channel of this color in an 8 bit value.
369 | @override
370 | int get blue => (0x000000ff & value) >> 0;
371 | }
372 |
373 | /// Help to reuse colors after rendering
374 | class ParticleRect extends Rect {
375 | /// The offset of the left edge of this rectangle from the x axis.
376 | @override
377 | double left = 0;
378 |
379 | /// The offset of the top edge of this rectangle from the y axis.
380 | @override
381 | double top = 0;
382 |
383 | /// The offset of the right edge of this rectangle from the x axis.
384 | @override
385 | double right = 0;
386 |
387 | /// The offset of the bottom edge of this rectangle from the y axis.
388 | @override
389 | double bottom = 0;
390 |
391 | /// Creates a [ParticleRect] from the left, top, width, and height parameters.
392 | ///
393 | /// The [left] parameter represents the x-coordinate of the left edge of the
394 | /// rectangle. The [top] parameter represents the y-coordinate of the top
395 | /// edge of the rectangle. The [width] parameter represents the width of the
396 | /// rectangle. The [height] parameter represents the height of the rectangle.
397 | ParticleRect.fromLTWH(super.left, super.top, super.width, super.height)
398 | : super.fromLTWH();
399 |
400 | /// Updates the particle's rectangle by setting the right and bottom edges based on the given width and height.
401 | ///
402 | /// The [width] parameter represents the new width of the rectangle.
403 | /// The [height] parameter represents the new height of the rectangle.
404 | void update(int width, int height) {
405 | right = left + width;
406 | bottom = top + height;
407 | }
408 | }
409 |
--------------------------------------------------------------------------------
/lib/src/particular_configs.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as math;
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:particular/particular.dart';
5 |
6 | /// Configs for managing parameters and behavior of a particle system.
7 | class ParticularConfigs {
8 | /// The default duration of a particle system.
9 | static int defaultDuration = 5000;
10 |
11 | /// The maximum duration of a particle system.
12 | static int maxDuration = 30000;
13 |
14 | /// The duration that we wait before we loop the particle system.
15 | static int endLoopPadding = 1000;
16 |
17 | /// Gets the start color of particles.
18 | Color getStartColor() => _getVariantColor(startColor, startColorVariance);
19 |
20 | /// Gets the finish color of particles.
21 | Color getFinishColor() => _getVariantColor(finishColor, finishColorVariance);
22 |
23 | /// Gets the lifespan of particles.
24 | int getLifespan() => _computeVariance(lifespan, lifespanVariance).round();
25 |
26 | /// Gets the emitter rect (based on the source position variance).
27 | Rect _getEmitterRect(double scaleFactor) {
28 | final width = sourcePositionVarianceX * scaleFactor;
29 | final height = sourcePositionVarianceY * scaleFactor;
30 |
31 | return Rect.fromLTWH(
32 | emitterX - width / 2,
33 | emitterY - height / 2,
34 | width,
35 | height,
36 | );
37 | }
38 |
39 | ({double x, double y}) getEmitterPosition(double scaleFactor) {
40 | final rotation = sourcePositionRotation * math.pi / 180;
41 | final rect = _getEmitterRect(scaleFactor);
42 | final centerX = rect.left + rect.width / 2;
43 | final centerY = rect.top + rect.height / 2;
44 |
45 | // Generate random point in a non-rotated rectangle
46 | final localX = math.Random().nextDouble() * rect.width - rect.width / 2;
47 | final localY = math.Random().nextDouble() * rect.height - rect.height / 2;
48 |
49 | // Rotate the point
50 | final rotatedX = localX * math.cos(rotation) - localY * math.sin(rotation);
51 | final rotatedY = localX * math.sin(rotation) + localY * math.cos(rotation);
52 |
53 | // Translate back to the world coordinate space
54 | final randomX = centerX + rotatedX;
55 | final randomY = centerY + rotatedY;
56 |
57 | return (x: randomX, y: randomY);
58 | }
59 |
60 | /// Gets the start size of particles.
61 | double getStartSize(double scaleFactor) =>
62 | _getVariantDouble(startSize, startSizeVariance, scaleFactor);
63 |
64 | /// Gets the finish size of particles.
65 | double getFinishSize(double scaleFactor) =>
66 | _getVariantDouble(finishSize, finishSizeVariance, scaleFactor);
67 |
68 | /// Gets the speed of particles.
69 | double getSpeed(double scaleFactor) =>
70 | _getVariantDouble(speed, speedVariance, scaleFactor);
71 |
72 | /// Gets the emission angle of particles.
73 | double getAngle() => _getVariantDouble(angle, angleVariance);
74 |
75 | /// Gets the minimum radius of particles.
76 | double getMinRadius(double scaleFactor) =>
77 | _getVariantDouble(minRadius, minRadiusVariance, scaleFactor);
78 |
79 | /// Gets the maximum radius of particles.
80 | double getMaxRadius(double scaleFactor) =>
81 | _getVariantDouble(maxRadius, maxRadiusVariance, scaleFactor);
82 |
83 | /// Gets the rotation rate of particles per second.
84 | double getRotatePerSecond() =>
85 | _getVariantDouble(rotatePerSecond, rotatePerSecondVariance);
86 |
87 | /// Gets the start rotation of particles.
88 | double getStartRotaion() =>
89 | _getVariantDouble(startRotation, startRotationVariance);
90 |
91 | /// Gets the end rotation of particles.
92 | double getFinishRotaion() =>
93 | _getVariantDouble(finishRotation, finishRotationVariance);
94 |
95 | /// Gets the radial acceleration of particles.
96 | double getRadialAcceleration() =>
97 | _getVariantDouble(radialAcceleration, radialAccelerationVariance);
98 |
99 | /// Gets the tangential acceleration of particles.
100 | double getTangentialAcceleration() =>
101 | _getVariantDouble(tangentialAcceleration, tangentialAccelerationVariance);
102 |
103 | /// Computes a value with a random variance.
104 | num _computeVariance(num base, num variance, [num coef = 1]) {
105 | if (variance == 0) {
106 | return base * coef;
107 | }
108 | final randomFactor = math.Random().nextDouble() * 2.0 - 1.0;
109 | return (base + variance * randomFactor) * coef;
110 | }
111 |
112 | /// Compounnd base double with a random variance double.
113 | double _getVariantDouble(num base, num variance, [num coef = 1]) =>
114 | _computeVariance(base, variance, coef).toDouble();
115 |
116 | /// Compounnd base color with a random variance color.
117 | Color _getVariantColor(ARGB base, ARGB variance) {
118 | var alpha = _computeVariance(base.a, variance.a, 255).clamp(0, 255).round();
119 | var red = _computeVariance(base.r, variance.r, 255).clamp(0, 255).round();
120 | var green = _computeVariance(base.g, variance.g, 255).clamp(0, 255).round();
121 | var blue = _computeVariance(base.b, variance.b, 255).clamp(0, 255).round();
122 | return Color.fromARGB(alpha, red, green, blue);
123 | }
124 |
125 | /// The name of the config.
126 | String configName = "";
127 |
128 | /// The name of the texture
129 | String textureFileName = "texture.png";
130 |
131 | /// The start time of the particle system in milliseconds.
132 | int startTime = 0;
133 |
134 | /// The end time of the particle system in milliseconds, -1 for infinite.
135 | int endTime = -1;
136 |
137 | /// The lifespan of particles in milliseconds.
138 | int lifespan = 1000;
139 |
140 | /// The variance of the lifespan of particles in milliseconds.
141 | int lifespanVariance = 0;
142 |
143 | /// The maximum number of particles.
144 | int maxParticles = 100;
145 |
146 | /// The blend mode value of textures.
147 | BlendMode textureBlendMode = BlendMode.srcOver;
148 |
149 | /// The blend mode value for rendering.
150 | BlendMode renderBlendMode = BlendMode.srcIn;
151 |
152 | /// The source blend mode function.
153 | BlendFunction blendFunctionSource = BlendFunction.one;
154 |
155 | /// The destination blend mode function.
156 | BlendFunction blendFunctionDestination = BlendFunction.oneMinusSourceAlpha;
157 |
158 | /// The start color of particles.
159 | ARGB startColor = ARGB(1, 1, 1, 1);
160 |
161 | /// The start color variance of particles.
162 | ARGB startColorVariance = ARGB(0, 0, 0, 0);
163 |
164 | /// The finish color of particles.
165 | ARGB finishColor = ARGB(0, 1, 1, 1);
166 |
167 | /// The finish color variance of particles.
168 | ARGB finishColorVariance = ARGB(0, 0, 0, 0);
169 |
170 | /// The emitter position along the x-axis.
171 | num emitterX = 200;
172 |
173 | /// The emitter position along the y-axis.
174 | num emitterY = 200;
175 |
176 | /// The variance of the source position along the x-axis.
177 | num sourcePositionVarianceX = 0;
178 |
179 | /// The variance of the source position along the y-axis.
180 | num sourcePositionVarianceY = 0;
181 |
182 | /// The rotation of the source position
183 | num sourcePositionRotation = 0;
184 |
185 | /// The start size of particles.
186 | num startSize = 30;
187 |
188 | /// The start size variance of particles.
189 | num startSizeVariance = 0;
190 |
191 | /// The finish size of particles.
192 | num finishSize = 0;
193 |
194 | /// The finish size variance of particles.
195 | num finishSizeVariance = 0;
196 |
197 | /// The speed of particles.
198 | num speed = 200;
199 |
200 | /// The variance of the speed of particles.
201 | num speedVariance = 0;
202 |
203 | /// The gravity along the x-axis.
204 | num gravityX = 0;
205 |
206 | /// The gravity along the y-axis.
207 | num gravityY = 0;
208 |
209 | /// The initial angle of particle emission in degrees.
210 | num angle = 0;
211 |
212 | /// The variance of the initial angle of particle emission in degrees.
213 | num angleVariance = 360;
214 |
215 | /// The minimum radius of particle emission.
216 | num minRadius = 0;
217 |
218 | /// The variance of the minimum radius of particle emission.
219 | num minRadiusVariance = 0;
220 |
221 | /// The maximum radius of particle emission.
222 | num maxRadius = 0;
223 |
224 | /// The variance of the maximum radius of particle emission.
225 | num maxRadiusVariance = 0;
226 |
227 | /// The rotation rate of particles per second.
228 | num rotatePerSecond = 0;
229 |
230 | /// The variance of the rotation rate of particles per second.
231 | num rotatePerSecondVariance = 0;
232 |
233 | /// The start rotation of particles.
234 | num startRotation = 0;
235 |
236 | /// The start rotation variance of particles.
237 | num startRotationVariance = 0;
238 |
239 | /// The final rotation of particles.
240 | num finishRotation = 0;
241 |
242 | /// The final rotation variance of particles.
243 | num finishRotationVariance = 0;
244 |
245 | /// The radial acceleration of particles.
246 | num radialAcceleration = 0;
247 |
248 | /// The variance of the radial acceleration of particles.
249 | num radialAccelerationVariance = 0;
250 |
251 | /// The tangential acceleration of particles.
252 | num tangentialAcceleration = 0;
253 |
254 | /// The variance of the tangential acceleration of particles.
255 | num tangentialAccelerationVariance = 0;
256 |
257 | /// The type of emitter (gravity or radius).
258 | EmitterType emitterType = EmitterType.gravity;
259 |
260 | /// The map of notifiers
261 | final Map _notifiers = {};
262 |
263 | /// Get notifier
264 | ChangeNotifier getNotifier(String key) =>
265 | _notifiers[key] ??= ChangeNotifier();
266 |
267 | /// First time initialize controller
268 | ParticularConfigs.initialize({Map? configs}) {
269 | if (configs == null) return;
270 | update(
271 | configName: configs["configName"],
272 | emitterType: EmitterType.values[configs["emitterType"]],
273 | blendFunctionSource:
274 | BlendFunction.fromValue(configs["blendFuncSource"] ?? 0),
275 | blendFunctionDestination:
276 | BlendFunction.fromValue(configs["blendFuncDestination"] ?? 0),
277 | renderBlendMode: BlendMode.values[configs["renderBlendMode"] ?? 0],
278 | textureBlendMode: BlendMode.values[configs["textureBlendMode"] ?? 0],
279 | startColor: ARGB.fromMap(configs, "startColor"),
280 | startColorVariance: ARGB.fromMap(configs, "startColorVariance"),
281 | finishColor: ARGB.fromMap(configs, "finishColor"),
282 | finishColorVariance: ARGB.fromMap(configs, "finishColorVariance"),
283 | lifespan: (configs["particleLifespan"] * 1000).round(),
284 | lifespanVariance: (configs["particleLifespanVariance"] * 1000).round(),
285 | startTime: ((configs["startTime"] ?? 0) * 1000).round(),
286 | endTime:
287 | (configs["duration"] * (configs["duration"] > -1 ? 1000 : 1)).round(),
288 | maxParticles: configs["maxParticles"],
289 | sourcePositionVarianceX: configs["sourcePositionVariancex"],
290 | sourcePositionVarianceY: configs["sourcePositionVariancey"],
291 | sourcePositionRotation: configs["sourcePositionRotation"],
292 | startSize: configs["startParticleSize"],
293 | startSizeVariance: configs["startParticleSizeVariance"],
294 | finishSize: configs["finishParticleSize"],
295 | finishSizeVariance: configs["finishParticleSizeVariance"],
296 | speed: configs["speed"],
297 | speedVariance: configs["speedVariance"],
298 | gravityX: configs["gravityx"],
299 | gravityY: configs["gravityy"],
300 | emitterX: configs["emitterX"],
301 | emitterY: configs["emitterY"],
302 | angle: configs["angle"],
303 | angleVariance: configs["angleVariance"],
304 | minRadius: configs["minRadius"],
305 | minRadiusVariance: configs["minRadiusVariance"],
306 | maxRadius: configs["maxRadius"],
307 | maxRadiusVariance: configs["maxRadiusVariance"],
308 | rotatePerSecond: configs["rotatePerSecond"],
309 | rotatePerSecondVariance: configs["rotatePerSecondVariance"],
310 | startRotation: configs["rotationStart"],
311 | startRotationVariance: configs["rotationStartVariance"],
312 | finishRotation: configs["rotationEnd"],
313 | finishRotationVariance: configs["rotationEndVariance"],
314 | radialAcceleration: configs["radialAcceleration"],
315 | radialAccelerationVariance: configs["radialAccelVariance"],
316 | tangentialAcceleration: configs["tangentialAcceleration"],
317 | tangentialAccelerationVariance: configs["tangentialAccelVariance"],
318 | );
319 | }
320 |
321 | /// particle system updater method
322 | void update({
323 | String? configName,
324 | String? textureFileName,
325 | EmitterType? emitterType,
326 | BlendMode? renderBlendMode,
327 | BlendMode? textureBlendMode,
328 | BlendFunction? blendFunctionSource,
329 | BlendFunction? blendFunctionDestination,
330 | int? startTime,
331 | int? endTime,
332 | int? lifespan,
333 | int? lifespanVariance,
334 | int? maxParticles,
335 | ARGB? startColor,
336 | ARGB? startColorVariance,
337 | ARGB? finishColor,
338 | ARGB? finishColorVariance,
339 | num? sourcePositionVarianceX,
340 | num? sourcePositionVarianceY,
341 | num? sourcePositionRotation,
342 | num? startSize,
343 | num? startSizeVariance,
344 | num? angle,
345 | num? angleVariance,
346 | num? finishSize,
347 | num? finishSizeVariance,
348 | num? speed,
349 | num? speedVariance,
350 | num? emitterX,
351 | num? emitterY,
352 | num? gravityX,
353 | num? gravityY,
354 | num? minRadius,
355 | num? minRadiusVariance,
356 | num? maxRadius,
357 | num? maxRadiusVariance,
358 | num? rotatePerSecond,
359 | num? rotatePerSecondVariance,
360 | num? startRotation,
361 | num? startRotationVariance,
362 | num? finishRotation,
363 | num? finishRotationVariance,
364 | num? radialAcceleration,
365 | num? radialAccelerationVariance,
366 | num? tangentialAcceleration,
367 | num? tangentialAccelerationVariance,
368 | }) {
369 | if (configName != null) {
370 | this.configName = configName;
371 | }
372 | if (textureFileName != null) {
373 | this.textureFileName = textureFileName;
374 | }
375 | if (emitterType != null) {
376 | this.emitterType = emitterType;
377 | }
378 | if (lifespan != null) {
379 | this.lifespan = lifespan;
380 | }
381 | if (lifespanVariance != null) {
382 | this.lifespanVariance = lifespanVariance;
383 | }
384 | if (startTime != null) {
385 | this.startTime = startTime.clamp(
386 | 0, this.endTime < 0 ? maxDuration : this.endTime - 50);
387 | }
388 | if (endTime != null) {
389 | this.endTime =
390 | endTime.clamp(this.startTime > 0 ? this.startTime : -1, maxDuration);
391 | }
392 | if (maxParticles != null) {
393 | this.maxParticles = maxParticles;
394 | }
395 |
396 | if (textureBlendMode != null) {
397 | this.textureBlendMode = textureBlendMode;
398 | }
399 | if (renderBlendMode != null) {
400 | this.renderBlendMode = renderBlendMode;
401 | }
402 | if (blendFunctionSource != null) {
403 | this.blendFunctionSource = blendFunctionSource;
404 | }
405 | if (blendFunctionDestination != null) {
406 | this.blendFunctionDestination = blendFunctionDestination;
407 | }
408 |
409 | if (startColor != null) {
410 | this.startColor = startColor;
411 | }
412 | if (startColorVariance != null) {
413 | this.startColorVariance = startColorVariance;
414 | }
415 | if (finishColor != null) {
416 | this.finishColor = finishColor;
417 | }
418 | if (finishColorVariance != null) {
419 | this.finishColorVariance = finishColorVariance;
420 | }
421 |
422 | if (sourcePositionVarianceX != null) {
423 | this.sourcePositionVarianceX = sourcePositionVarianceX;
424 | }
425 | if (sourcePositionVarianceY != null) {
426 | this.sourcePositionVarianceY = sourcePositionVarianceY;
427 | }
428 |
429 | if (sourcePositionRotation != null) {
430 | this.sourcePositionRotation = sourcePositionRotation;
431 | }
432 |
433 | if (startSize != null) {
434 | this.startSize = startSize;
435 | }
436 | if (startSizeVariance != null) {
437 | this.startSizeVariance = startSizeVariance;
438 | }
439 | if (finishSize != null) {
440 | this.finishSize = finishSize;
441 | }
442 | if (finishSizeVariance != null) {
443 | this.finishSizeVariance = finishSizeVariance;
444 | }
445 |
446 | if (speed != null) {
447 | this.speed = speed;
448 | }
449 | if (speedVariance != null) {
450 | this.speedVariance = speedVariance;
451 | }
452 | if (angle != null) {
453 | this.angle = angle;
454 | }
455 | if (angleVariance != null) {
456 | this.angleVariance = angleVariance;
457 | }
458 | if (emitterX != null) {
459 | this.emitterX = emitterX;
460 | }
461 | if (emitterY != null) {
462 | this.emitterY = emitterY;
463 | }
464 | if (gravityX != null) {
465 | this.gravityX = gravityX;
466 | }
467 | if (gravityY != null) {
468 | this.gravityY = gravityY;
469 | }
470 | if (minRadius != null) {
471 | this.minRadius = minRadius;
472 | }
473 | if (minRadiusVariance != null) {
474 | this.minRadiusVariance = minRadiusVariance;
475 | }
476 | if (maxRadius != null) {
477 | this.maxRadius = maxRadius;
478 | }
479 | if (maxRadiusVariance != null) {
480 | this.maxRadiusVariance = maxRadiusVariance;
481 | }
482 |
483 | if (rotatePerSecond != null) {
484 | this.rotatePerSecond = rotatePerSecond;
485 | }
486 | if (rotatePerSecondVariance != null) {
487 | this.rotatePerSecondVariance = rotatePerSecondVariance;
488 | }
489 | if (startRotation != null) {
490 | this.startRotation = startRotation;
491 | }
492 | if (startRotationVariance != null) {
493 | this.startRotationVariance = startRotationVariance;
494 | }
495 | if (finishRotation != null) {
496 | this.finishRotation = finishRotation;
497 | }
498 | if (finishRotationVariance != null) {
499 | this.finishRotationVariance = finishRotationVariance;
500 | }
501 |
502 | if (radialAcceleration != null) {
503 | this.radialAcceleration = radialAcceleration;
504 | }
505 | if (radialAccelerationVariance != null) {
506 | this.radialAccelerationVariance = radialAccelerationVariance;
507 | }
508 | if (tangentialAcceleration != null) {
509 | this.tangentialAcceleration = tangentialAcceleration;
510 | }
511 | if (tangentialAccelerationVariance != null) {
512 | this.tangentialAccelerationVariance = tangentialAccelerationVariance;
513 | }
514 | }
515 |
516 | /// Updates the controller from a map
517 | void updateWith(Map args) {
518 | update(
519 | configName: args["configName"],
520 | textureFileName: args["textureFileName"],
521 | emitterType: args["emitterType"],
522 | renderBlendMode: args["renderBlendMode"],
523 | textureBlendMode: args["textureBlendMode"],
524 | blendFunctionSource: args["blendFunctionSource"],
525 | blendFunctionDestination: args["blendFunctionDestination"],
526 | startTime: args["startTime"],
527 | endTime: args["endTime"],
528 | lifespan: args["lifespan"],
529 | lifespanVariance: args["lifespanVariance"],
530 | maxParticles: args["maxParticles"],
531 | startColor: args["startColor"],
532 | startColorVariance: args["startColorVariance"],
533 | finishColor: args["finishColor"],
534 | finishColorVariance: args["finishColorVariance"],
535 | sourcePositionVarianceX: args["sourcePositionVarianceX"],
536 | sourcePositionVarianceY: args["sourcePositionVarianceY"],
537 | sourcePositionRotation: args["sourcePositionRotation"],
538 | startSize: args["startSize"],
539 | startSizeVariance: args["startSizeVariance"],
540 | angle: _loopClamp(args["angle"], -180, 180),
541 | angleVariance: _clamp(args["angleVariance"], 0, 360),
542 | finishSize: args["finishSize"],
543 | finishSizeVariance: args["finishSizeVariance"],
544 | speed: args["speed"],
545 | speedVariance: args["speedVariance"],
546 | emitterX: args["emitterX"],
547 | emitterY: args["emitterY"],
548 | gravityX: args["gravityX"],
549 | gravityY: args["gravityY"],
550 | minRadius: args["minRadius"],
551 | minRadiusVariance: args["minRadiusVariance"],
552 | maxRadius: args["maxRadius"],
553 | maxRadiusVariance: args["maxRadiusVariance"],
554 | rotatePerSecond: args["rotatePerSecond"],
555 | rotatePerSecondVariance: args["rotatePerSecondVariance"],
556 | startRotation: args["startRotation"],
557 | startRotationVariance: args["startRotationVariance"],
558 | finishRotation: args["finishRotation"],
559 | finishRotationVariance: args["finishRotationVariance"],
560 | radialAcceleration: args["radialAcceleration"],
561 | radialAccelerationVariance: args["radialAccelerationVariance"],
562 | tangentialAcceleration: args["tangentialAcceleration"],
563 | tangentialAccelerationVariance: args["tangentialAccelerationVariance"],
564 | );
565 | for (var key in args.keys) {
566 | // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
567 | getNotifier(key).notifyListeners();
568 | }
569 | }
570 |
571 | /// Circulate the given [value] between the specified [min] and [max]
572 | /// boundaries.
573 | ///
574 | /// Parameters:
575 | /// - [value]: The value to be clamped.
576 | /// - [min]: The lower boundary of the range.
577 | /// - [max]: The upper boundary of the range.
578 | ///
579 | /// Returns:
580 | /// - The clamped value.
581 | num? _loopClamp(num? value, int min, int max) {
582 | if (value == null) return null;
583 | var diff = max - min;
584 | // var half = (diff * 0.5).round();
585 | while (value! < min) {
586 | value += diff;
587 | }
588 | while (value! > max) {
589 | value -= diff;
590 | }
591 | return value;
592 | }
593 |
594 | /// Clamp value between min and max
595 | num? _clamp(num? value, int min, int max) {
596 | if (value == null) return null;
597 | return value.clamp(min, max);
598 | }
599 |
600 | void dispose() {
601 | for (var notifier in _notifiers.values) {
602 | notifier.dispose();
603 | }
604 | _notifiers.clear();
605 | }
606 | }
607 |
608 | /// The wrapper class for colors
609 | class ARGB {
610 | /// Represents Alpha channel
611 | num a;
612 |
613 | /// Represents Red channel
614 | num r;
615 |
616 | /// Represents Green channel
617 | num g;
618 |
619 | /// Represents Blue channel
620 | num b;
621 |
622 | /// Create ARGB class
623 | ARGB(this.a, this.r, this.g, this.b);
624 |
625 | /// Create ARGB class and assign members with data
626 | static ARGB fromMap(Map map, String name) {
627 | var color = ARGB(1, 1, 1, 1);
628 | color.a = map["${name}Alpha"];
629 | color.r = map["${name}Red"];
630 | color.g = map["${name}Green"];
631 | color.b = map["${name}Blue"];
632 | return color;
633 | }
634 |
635 | /// Returns a Color object with ARGB values calculated from the current ARGB instance.
636 | Color getColor() => Color.fromARGB((a * 255).toInt(), (r * 255).toInt(),
637 | (g * 255).toInt(), (b * 255).toInt());
638 | }
639 |
--------------------------------------------------------------------------------
/lib/src/particular_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 | import 'dart:ui' as ui;
3 |
4 | import 'package:flutter/foundation.dart';
5 | import 'package:flutter/scheduler.dart';
6 | import 'package:flutter/services.dart';
7 | import 'package:particular/particular.dart';
8 |
9 | /// The type of notifiers
10 | enum NotifierType { time, layer }
11 |
12 | /// The controller for the particle system.
13 | class ParticularController {
14 | /// The list of layers in the particle system.
15 | final List _layers = [];
16 |
17 | /// Get layers
18 | List get layers => _layers;
19 |
20 | /// The index of the selected layer.
21 | int selectedLayerIndex = 0;
22 |
23 | /// The ticker for the particle system.
24 | ParticularLayer? get selectedLayer =>
25 | _layers.isEmpty ? null : _layers[selectedLayerIndex];
26 |
27 | /// Whether the particle system is empty.
28 | bool get isEmpty => _layers.isEmpty;
29 |
30 | /// The map of notifiers
31 | final Map _notifiers = {};
32 |
33 | /// Get notifier
34 | ChangeNotifier getNotifier(NotifierType key) =>
35 | _notifiers[key] ??= ChangeNotifier();
36 |
37 | /// Notifies listeners that the duration of the particle system has changed.
38 | void notify(NotifierType key) =>
39 | // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
40 | getNotifier(key).notifyListeners();
41 |
42 | /// The delta time of the particle system in milliseconds.
43 | int deltaTime = 0;
44 |
45 | /// The elapsed time of the particle system in milliseconds.
46 | int elapsedTime = 0;
47 |
48 | /// The duration of the particle system in milliseconds.
49 | int get timelineDuration {
50 | if (_layers.isEmpty) return ParticularConfigs.defaultDuration;
51 | var max = _layers
52 | .reduce((l, r) => l.configs.endTime > r.configs.endTime ? l : r)
53 | .configs
54 | .endTime;
55 | if (max < ParticularConfigs.defaultDuration) {
56 | return ParticularConfigs.defaultDuration;
57 | }
58 | return max + 100;
59 | }
60 |
61 | double _particlesPerTick = 0;
62 |
63 | /// The ticker for the particle system.
64 | Ticker? _ticker;
65 |
66 | bool _isLooping = false;
67 |
68 | bool get isLooping => _isLooping;
69 |
70 | bool get _hasInfiniteLayer =>
71 | _layers.any((layer) => layer.configs.endTime < 0);
72 |
73 | // Finds the farthest end time of the layers
74 | int get farthestEndTime {
75 | int lastEndAt = 0;
76 | for (var layer in _layers) {
77 | if (layer.configs.endTime > lastEndAt) {
78 | lastEndAt = layer.configs.endTime;
79 | }
80 | }
81 | return lastEndAt;
82 | }
83 |
84 | /// Updates the particle system's delta time and elapsed time based on the given [elapsed] duration.
85 | ///
86 | /// This function is called periodically to update the particle system's state. It calculates the
87 | /// delta time and current elapsed time.
88 | ///
89 | /// Parameters:
90 | /// - elapsed: The duration since the last update.
91 | void _onTick(Duration elapsed) {
92 | deltaTime = elapsed.inMilliseconds - elapsedTime;
93 | elapsedTime = elapsed.inMilliseconds;
94 |
95 | // Spawn particles
96 | for (var layer in _layers) {
97 | var configs = layer.configs;
98 | var duration =
99 | configs.endTime > 0 ? configs.endTime - configs.startTime : 1000;
100 | if (elapsedTime >= configs.startTime &&
101 | (configs.endTime < 0 || elapsedTime < configs.endTime)) {
102 | _particlesPerTick += (deltaTime * configs.maxParticles / duration);
103 | var floor = _particlesPerTick.floor();
104 | for (var i = 0; i < floor; i++) {
105 | layer.spawn(age: (i * deltaTime / _particlesPerTick).round());
106 | }
107 | _particlesPerTick -= floor;
108 | }
109 | }
110 |
111 | // Let's loop
112 | _tryToLoop();
113 |
114 | notify(NotifierType.time);
115 | }
116 |
117 | /// Here we try to loop the particle system
118 | void _tryToLoop() {
119 | if (!_isLooping) {
120 | return;
121 | }
122 |
123 | int loopAt = timelineDuration;
124 |
125 | if (!_hasInfiniteLayer && elapsedTime > farthestEndTime) {
126 | loopAt = min(
127 | farthestEndTime + ParticularConfigs.endLoopPadding,
128 | timelineDuration,
129 | );
130 | }
131 | if (elapsedTime > loopAt) {
132 | resetTick();
133 | }
134 | }
135 |
136 | /// Notifies listeners that the duration of the particle system has changed.
137 | void _onDurationChange() => notify(NotifierType.time);
138 |
139 | /// Resets the tick of the particle system.
140 | void resetTick() {
141 | _ticker?.stop();
142 | elapsedTime = 0;
143 | _ticker?.start();
144 | }
145 |
146 | /// Adds a new particle system to the application.
147 | @Deprecated('Use [addConfigLayer]')
148 | Future addLayer({
149 | dynamic configsData,
150 | }) async {
151 | await addConfigLayer(configsData: configsData);
152 | }
153 |
154 | /// Adds one or more particle systems to the application.
155 | ///
156 | /// The [configs] parameter can be either a single configuration map or a
157 | /// list of configuration maps. If [configs] is a list, each configuration
158 | /// map in the list will be added as a separate particle system.
159 | Future addConfigLayer({
160 | dynamic configsData,
161 | }) async {
162 | // If the configs parameter is a list, iterate over each configuration
163 | // map and add it as a separate particle system.
164 | if (configsData is List) {
165 | for (var i = 0; i < configsData.length; i++) {
166 | await addConfigs(configsData: configsData[i]);
167 | }
168 | } else {
169 | await addConfigs(configsData: configsData);
170 | }
171 | }
172 |
173 | /// Adds a new particle system to the application.
174 | @protected
175 | Future addConfigs({Map? configsData}) async {
176 | ByteData bytes;
177 |
178 | /// Load particle texture
179 | ui.Image? texture;
180 | try {
181 | if (configsData != null && configsData.containsKey("textureFileName")) {
182 | bytes =
183 | await rootBundle.load("assets/${configsData["textureFileName"]}");
184 | texture = await loadUIImage(bytes.buffer.asUint8List());
185 | }
186 | } catch (e) {
187 | debugPrint(e.toString());
188 | }
189 | final configs = ParticularConfigs.initialize(configs: configsData);
190 | final layer = ParticularLayer(texture: texture!, configs: configs);
191 | addParticularLayer(layer);
192 | }
193 |
194 | /// Adds a new particle system to the application.
195 | ///
196 | /// The [configs] parameter can be either a single configuration map or a
197 | /// list of configuration maps. If [configs] is a list, each configuration
198 | /// map in the list will be added as a separate particle system.
199 | void addParticularLayer(ParticularLayer layer) {
200 | layer.configs.getNotifier("duration").addListener(_onDurationChange);
201 | layer.configs.getNotifier("startTime").addListener(_onDurationChange);
202 | layer.index = _layers.length;
203 | selectedLayerIndex = layer.index;
204 |
205 | if (_ticker == null) {
206 | _ticker = Ticker(_onTick);
207 | _ticker!.start();
208 | }
209 |
210 | _layers.add(layer);
211 | notify(NotifierType.layer);
212 | }
213 |
214 | /// Selects the particle system at the given index.
215 | void selectLayerAt(int index) {
216 | selectedLayerIndex = index;
217 | notify(NotifierType.layer);
218 | }
219 |
220 | /// Removes the particle system at the given index.
221 | void removeLayerAt(int index) {
222 | _layers[index]
223 | .configs
224 | .getNotifier("duration")
225 | .removeListener(_onDurationChange);
226 | _layers[index]
227 | .configs
228 | .getNotifier("startTime")
229 | .removeListener(_onDurationChange);
230 | _layers.removeAt(index);
231 | if (selectedLayerIndex >= _layers.length) {
232 | selectedLayerIndex = _layers.length - 1;
233 | }
234 | notify(NotifierType.layer);
235 | }
236 |
237 | /// Reorders the particle system's items based on the new index.
238 | void reOrderLayer(int oldIndex, int newIndex) {
239 | if (oldIndex < newIndex) {
240 | newIndex -= 1;
241 | }
242 | final item = _layers.removeAt(oldIndex);
243 | _layers.insert(newIndex, item);
244 | selectedLayerIndex = newIndex;
245 | notify(NotifierType.layer);
246 | }
247 |
248 | /// Toggles the visibility of the particle system.
249 | void toggleVisibleLayer(int index) {
250 | // _layers[index].isVisible = !_layers[index].isVisible;
251 | notify(NotifierType.layer);
252 | }
253 |
254 | /// Toggles the looping state of the particle system.
255 | void setIsLooping(bool isLooping) {
256 | _isLooping = isLooping;
257 | notify(NotifierType.layer);
258 | }
259 |
260 | /// Disposes the controllers, notifiers and stops the ticker.
261 | /// If you are using frequently of layers or particulars,
262 | /// Its a good practice to dispose unused of them
263 | void dispose() {
264 | _ticker?.stop();
265 |
266 | for (var layer in _layers) {
267 | layer.configs.dispose();
268 | layer.texture.dispose();
269 | }
270 |
271 | for (var notifier in _notifiers.values) {
272 | notifier.dispose();
273 | }
274 | _notifiers.clear();
275 | }
276 | }
277 |
--------------------------------------------------------------------------------
/lib/src/particular_emitter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:particular/particular.dart';
3 |
4 | /// A widget that represents a particle system.
5 | class Particular extends StatefulWidget {
6 | /// The controller for the particle system.
7 | final ParticularController controller;
8 |
9 | /// Creates a [Particular] widget.
10 | const Particular({
11 | super.key,
12 | required this.controller,
13 | });
14 |
15 | /// Creates the state for the [Particular] widget.
16 | ///
17 | /// Returns a new instance of [_ParticularState].
18 | @override
19 | State createState() => _ParticularState();
20 | }
21 |
22 | /// The state for the [Particular] widget.
23 | class _ParticularState extends State {
24 | /// This method can potentially be called in every frame and should not have
25 | /// any side effects beyond building a widget.
26 | @override
27 | Widget build(BuildContext context) {
28 | return ListenableBuilder(
29 | listenable: widget.controller.getNotifier(NotifierType.time),
30 | builder: (context, _) {
31 | return SizedBox(
32 | child: CustomPaint(
33 | painter: ParticlePainter(
34 | controller: widget.controller,
35 | deltaTime: widget.controller.deltaTime,
36 | ),
37 | ),
38 | );
39 | },
40 | );
41 | }
42 | }
43 |
44 | /// A custom painter for rendering particles.
45 | class ParticlePainter extends CustomPainter {
46 | /// The time difference between frames.
47 | final int deltaTime;
48 |
49 | /// The paint object for rendering particles.
50 | final Paint _paint = Paint();
51 |
52 | final ParticularController controller;
53 |
54 | /// Creates a [ParticlePainter] with the specified parameters.
55 | ParticlePainter({
56 | required this.deltaTime,
57 | required this.controller,
58 | });
59 |
60 | /// Draws many parts of an image - the [atlas] - onto the canvas.
61 | @override
62 | void paint(Canvas canvas, Size size) {
63 | var allParticlesDead = true;
64 | for (var layer in controller.layers) {
65 | _paint.blendMode = layer.configs.textureBlendMode;
66 | for (var i = 0; i < layer.particles.length; i++) {
67 | var particle = layer.particles[i];
68 | layer.rectangles[i].update(layer.texture.width, layer.texture.height);
69 | particle.update(deltaTime);
70 | particle.transform.update(
71 | rotation: particle.rotation,
72 | translateX: particle.x,
73 | translateY: particle.y,
74 | anchorX: layer.texture.width * 0.5,
75 | anchorY: layer.texture.height * 0.5,
76 | scale: particle.size / layer.texture.width,
77 | );
78 |
79 | if (particle.isAlive && particle.isDyingTime()) {
80 | layer.deadParticleIndices.add(i);
81 | particle.isAlive = false;
82 | particle.color
83 | .update(0, particle.color.r, particle.color.g, particle.color.b);
84 | } else {
85 | allParticlesDead = false;
86 | }
87 | }
88 | canvas.drawAtlas(layer.texture, layer.transforms, layer.rectangles,
89 | layer.colors, layer.configs.renderBlendMode, null, _paint);
90 |
91 | if (allParticlesDead) {
92 | layer.onFinished?.call();
93 | }
94 | }
95 | }
96 |
97 | /// If the method returns false, then the [paint] call might be optimized
98 | /// away.
99 | @override
100 | bool shouldRepaint(CustomPainter oldDelegate) => true;
101 | }
102 |
--------------------------------------------------------------------------------
/lib/src/particular_layer.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui' as ui;
2 |
3 | import 'package:particular/particular.dart';
4 |
5 | class ParticularLayer {
6 | /// The index of the particle system.
7 | int index = 0;
8 |
9 | /// The rectangles representing particles.
10 | final List rectangles = [];
11 |
12 | /// The particles in the system.
13 | final List particles = [];
14 |
15 | /// The colors of particles.
16 | final List colors = [];
17 |
18 | /// The indices of the dead particles.
19 | final List deadParticleIndices = [];
20 |
21 | /// The transforms of particles.
22 | final List transforms = [];
23 |
24 | /// The configs for managing parameters and behavior of a particle system.
25 | final ParticularConfigs configs;
26 |
27 | /// The texture used for particles.
28 | ui.Image texture;
29 |
30 | /// A callback function called when particle rendering is finished.
31 | final Function()? onFinished;
32 |
33 | /// Creates a new instance of the ParticularLayer class.
34 | ParticularLayer(
35 | {required this.texture, required this.configs, this.onFinished});
36 |
37 | /// Spawns a particle.
38 | ///
39 | /// Spawns a particle object with the given age. If there are no dead particles in the
40 | /// pool, a new particle object is created and added to the pool. Otherwise, a dead
41 | /// particle object is resurrected and added to the pool. The particle object is
42 | /// initialized with the given parameters.
43 | ///
44 | /// Parameters:
45 | /// - age: The age of the particle. Defaults to 0.
46 | ///
47 | /// Returns: None.
48 | void spawn({int age = 0, double scaleFactor = 1.0}) {
49 | Particle particle;
50 | if (deadParticleIndices.isEmpty) {
51 | particle = Particle();
52 | colors.add(particle.color);
53 | transforms.add(particle.transform);
54 | rectangles.add(ParticleRect.fromLTWH(
55 | 0, 0, texture.width.toDouble(), texture.height.toDouble()));
56 | particles.add(particle);
57 | } else {
58 | particle = particles[deadParticleIndices.removeLast()];
59 | }
60 | final position = configs.getEmitterPosition(1);
61 | particle.initialize(
62 | age: age,
63 | emitterType: configs.emitterType,
64 | emitterX: position.x,
65 | emitterY: position.y,
66 | startSize: configs.getStartSize(1),
67 | finishSize: configs.getFinishSize(1),
68 | startColor: configs.getStartColor(),
69 | finishColor: configs.getFinishColor(),
70 | angle: configs.getAngle(),
71 | lifespan: configs.getLifespan(),
72 | speed: configs.getSpeed(scaleFactor),
73 | gravityX: configs.gravityX * scaleFactor,
74 | gravityY: configs.gravityY * scaleFactor,
75 | minRadius: configs.getMinRadius(1),
76 | maxRadius: configs.getMaxRadius(1),
77 | rotatePerSecond: configs.getRotatePerSecond(),
78 | startRotation: configs.getStartRotaion(),
79 | finishRotation: configs.getFinishRotaion(),
80 | radialAcceleration: configs.getRadialAcceleration(),
81 | tangentialAcceleration: configs.getTangentialAcceleration(),
82 | );
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/particular.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: particular
2 | description: "The Particular is a high performance particle effects flutter widget."
3 | version: 0.3.4
4 | homepage: https://manjav.github.io/particular
5 | repository: https://github.com/manjav/particular
6 | issue_tracker: https://github.com/manjav/particular/issues
7 | documentation: https://github.com/manjav/particular
8 |
9 | environment:
10 | sdk: '>=3.3.1 <4.0.0'
11 | flutter: '>=3.3.0'
12 |
13 | dependencies:
14 | flutter:
15 | sdk: flutter
16 | image: ^4.2.0
17 |
18 | dev_dependencies:
19 | flutter_test:
20 | sdk: flutter
21 | flutter_lints: ^5.0.0
22 |
23 | screenshots:
24 | - description: 'Particular Logo'
25 | path: repo_files/logo.png
26 |
27 | topics:
28 | - particle
29 | - particles
30 | - visualization
31 | - game
32 | - effects
--------------------------------------------------------------------------------
/repo_files/editor_left.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/editor_left.gif
--------------------------------------------------------------------------------
/repo_files/editor_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/editor_right.png
--------------------------------------------------------------------------------
/repo_files/example_firework.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/example_firework.webp
--------------------------------------------------------------------------------
/repo_files/example_galaxy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/example_galaxy.webp
--------------------------------------------------------------------------------
/repo_files/example_meteor.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/example_meteor.webp
--------------------------------------------------------------------------------
/repo_files/example_snow.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/example_snow.webp
--------------------------------------------------------------------------------
/repo_files/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manjav/particular/3fcce5dc2558895f20ff86ece35652cd79c972ca/repo_files/logo.png
--------------------------------------------------------------------------------
/test/particular_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 |
3 | void main() {
4 | test('getPlatformVersion', () async {
5 | expect('42', '42');
6 | });
7 | }
8 |
--------------------------------------------------------------------------------