├── .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 | Particular Logo 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 | Meteor 21 | Galaxy 22 | Snow 23 | Meteor 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 | ![Particular Logo](https://github.com/manjav/particular/raw/main/repo_files/logo.png) 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 | --------------------------------------------------------------------------------