diff --git a/doc/README.md b/doc/README.md index 877ee3c6c..83e66436e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -50,6 +50,7 @@ And start using it! - [Images, Sprites and Animations](images.md) - [Text Rendering](text.md) - [Colors and the Palette](palette.md) + - [Particles](particles.md) * Other Modules - [Util](util.md) - [Gamepad](gamepad.md) diff --git a/doc/examples/particles/.gitignore b/doc/examples/particles/.gitignore index 07488ba61..e3c6f9654 100644 --- a/doc/examples/particles/.gitignore +++ b/doc/examples/particles/.gitignore @@ -36,6 +36,9 @@ **/android/local.properties **/android/**/GeneratedPluginRegistrant.java +# Web related +**/web/** + # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 diff --git a/doc/examples/particles/assets/diamond.flr b/doc/examples/particles/assets/diamond.flr new file mode 100644 index 000000000..064d673da --- /dev/null +++ b/doc/examples/particles/assets/diamond.flr @@ -0,0 +1,799 @@ +{ + "version": 24, + "artboards": [ + { + "name": "Artboard", + "translation": [ + 0, + 0 + ], + "width": 250, + "height": 250, + "origin": [ + 0, + 0 + ], + "clipContents": true, + "color": [ + 0.364705890417099, + 0.364705890417099, + 0.364705890417099, + 1 + ], + "nodes": [ + { + "name": "Polygon", + "translation": [ + 125, + 125 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "isVisible": true, + "blendMode": 3, + "drawOrder": 1, + "transformAffectsStroke": false, + "type": "shape" + }, + { + "name": "Color", + "parent": 0, + "opacity": 1, + "color": [ + 0.4534761905670166, + 0.8476190567016602, + 0.7884976267814636, + 1 + ], + "fillRule": 1, + "type": "colorFill" + }, + { + "name": "Polygon Path", + "parent": 0, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "width": 250, + "height": 250, + "sides": 4, + "type": "polygon" + }, + { + "name": "globe", + "translation": [ + 125, + 125 + ], + "rotation": 0, + "scale": [ + 10, + 10 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "type": "node" + }, + { + "name": "Shape", + "parent": 3, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "isVisible": true, + "blendMode": 3, + "drawOrder": 2, + "transformAffectsStroke": false, + "type": "shape" + }, + { + "name": "Color", + "parent": 4, + "opacity": 1, + "color": [ + 0, + 0, + 0, + 1 + ], + "width": 2, + "cap": 0, + "join": 0, + "trim": 0, + "type": "colorStroke" + }, + { + "name": "Ellipse", + "parent": 4, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "width": 20, + "height": 20, + "type": "ellipse" + }, + { + "name": "Shape", + "parent": 3, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "isVisible": true, + "blendMode": 3, + "drawOrder": 3, + "transformAffectsStroke": false, + "type": "shape" + }, + { + "name": "Color", + "parent": 7, + "opacity": 1, + "color": [ + 0, + 0, + 0, + 1 + ], + "width": 2, + "cap": 0, + "join": 0, + "trim": 0, + "type": "colorStroke" + }, + { + "name": "Path", + "parent": 7, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "bones": [], + "isVisible": true, + "isClosed": true, + "points": [ + { + "pointType": 2, + "translation": [ + 0, + -10 + ], + "in": [ + 0, + -10 + ], + "out": [ + 2.5012803077697754, + -7.2616472244262695 + ] + }, + { + "pointType": 2, + "translation": [ + 4, + 1.7763568394002505e-15 + ], + "in": [ + 3.922752857208252, + -3.7079660892486572 + ], + "out": [ + 3.922752857208252, + 3.7079660892486572 + ] + }, + { + "pointType": 2, + "translation": [ + -2.6645352591003757e-14, + 10 + ], + "in": [ + 2.5012803077697754, + 7.2616472244262695 + ], + "out": [ + -2.5012803077697754, + 7.2616472244262695 + ] + }, + { + "pointType": 2, + "translation": [ + -4, + 1.7763568394002505e-15 + ], + "in": [ + -3.922752857208252, + 3.7079660892486572 + ], + "out": [ + -3.922752857208252, + -3.7079660892486572 + ] + }, + { + "pointType": 2, + "translation": [ + 3.019806626980426e-14, + -10 + ], + "in": [ + -2.5012803077697754, + -7.2616472244262695 + ], + "out": [ + 3.019806626980426e-14, + -10 + ] + } + ], + "type": "path" + }, + { + "name": "Ellipse", + "translation": [ + 125, + 125 + ], + "rotation": 0, + "scale": [ + 10, + 10 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "isVisible": true, + "blendMode": 3, + "drawOrder": 4, + "transformAffectsStroke": false, + "type": "shape" + }, + { + "name": "Color", + "parent": 10, + "opacity": 1, + "color": [ + 1, + 1, + 1, + 1 + ], + "width": 10, + "cap": 1, + "join": 0, + "trim": 1, + "start": 0, + "end": 0, + "offset": 0, + "type": "colorStroke" + }, + { + "name": "Ellipse Path", + "parent": 10, + "translation": [ + 0, + 0 + ], + "rotation": 0, + "scale": [ + 1, + 1 + ], + "opacity": 1, + "isCollapsed": false, + "clips": [], + "width": 14, + "height": 14, + "type": "ellipse" + } + ], + "animations": [ + { + "name": "Spin", + "fps": 60, + "duration": 10, + "isLooping": true, + "keyed": [ + { + "component": 0, + "scaleY": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + }, + { + "time": 2.5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.49699999999999966 + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + }, + { + "time": 7.766666666666667, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.49699999999999966 + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + } + ] + ], + "scaleX": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + }, + { + "time": 2.5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.5049999999999999 + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + }, + { + "time": 7.766666666666667, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.5049999999999999 + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + } + ] + ], + "posX": [ + [ + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 125 + } + ] + ] + }, + { + "component": 1, + "fillColor": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0.4534761905670166, + 0.6939033269882202, + 0.8476190567016602, + 1 + ] + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0.4773571491241455, + 0.776190459728241, + 0.6058554649353027, + 1 + ] + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0.4534761905670166, + 0.6939033269882202, + 0.8476190567016602, + 1 + ] + } + ] + ] + }, + { + "component": 3, + "rotation": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0 + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 6.283185307179586 + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0 + } + ] + ], + "posX": [ + [ + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 125 + } + ] + ] + }, + { + "component": 9, + "pathVertices": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0, + -10, + 0, + -10, + 2.5012803077697754, + -7.2616472244262695, + 4, + 1.7763568394002505e-15, + 3.922752857208252, + -3.7079660892486572, + 3.922752857208252, + 3.7079660892486572, + -2.6645352591003757e-14, + 10, + 2.5012803077697754, + 7.2616472244262695, + -2.5012803077697754, + 7.2616472244262695, + -4, + 1.7763568394002505e-15, + -3.922752857208252, + 3.7079660892486572, + -3.922752857208252, + -3.7079660892486572, + 3.019806626980426e-14, + -10, + -2.5012803077697754, + -7.2616472244262695, + 3.019806626980426e-14, + -10 + ] + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0, + -10, + 0, + -10, + -0.028131680563092232, + -0.08517590165138245, + 9.294120788574219, + -0.05882320925593376, + 9.216875076293945, + -3.766789197921753, + 9.216875076293945, + 3.6491427421569824, + -2.6645352591003757e-14, + 10, + -0.02813015505671501, + -0.03246975317597389, + -0.03069210611283779, + -0.09129314869642258, + -8.999999046325684, + 1.862645149230957e-7, + -8.92275333404541, + 3.70796537399292, + -8.92275333404541, + -3.707965612411499, + 3.019806626980426e-14, + -10, + -0.08951549977064133, + -0.14400005340576172, + 3.019806626980426e-14, + -10 + ] + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": [ + 0, + -10, + 0, + -10, + 2.5012803077697754, + -7.2616472244262695, + 4, + 1.7763568394002505e-15, + 3.922752857208252, + -3.7079660892486572, + 3.922752857208252, + 3.7079660892486572, + -2.6645352591003757e-14, + 10, + 2.5012803077697754, + 7.2616472244262695, + -2.5012803077697754, + 7.2616472244262695, + -4, + 1.7763568394002505e-15, + -3.922752857208252, + 3.7079660892486572, + -3.922752857208252, + -3.7079660892486572, + 3.019806626980426e-14, + -10, + -2.5012803077697754, + -7.2616472244262695, + 3.019806626980426e-14, + -10 + ] + } + ] + ] + }, + { + "component": 10 + }, + { + "component": 11, + "strokeStart": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0 + }, + { + "time": 2.5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.25 + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.5 + }, + { + "time": 7.533333333333333, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.6 + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + } + ] + ], + "strokeEnd": [ + [ + { + "time": 0, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0 + }, + { + "time": 2.5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.5 + }, + { + "time": 5, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.8 + }, + { + "time": 7.533333333333333, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 0.8 + }, + { + "time": 10, + "interpolatorType": 2, + "cubicX1": 0.5913978494623656, + "cubicY1": 0.0766129032258065, + "cubicX2": 0.17204301075268819, + "cubicY2": 1.1048387096774193, + "value": 1 + } + ] + ] + } + ], + "animationStart": 0, + "animationEnd": 10, + "type": "animation" + } + ], + "type": "artboard" + } + ] +} \ No newline at end of file diff --git a/doc/examples/particles/assets/images/boom3.png b/doc/examples/particles/assets/images/boom3.png new file mode 100644 index 000000000..8ada14a70 Binary files /dev/null and b/doc/examples/particles/assets/images/boom3.png differ diff --git a/doc/examples/particles/lib/main.dart b/doc/examples/particles/lib/main.dart index 45494f5f6..7fb32d762 100644 --- a/doc/examples/particles/lib/main.dart +++ b/doc/examples/particles/lib/main.dart @@ -1,33 +1,36 @@ import 'dart:async'; import 'dart:math'; +import 'dart:ui'; +import 'package:flame/animation.dart'; +import 'package:flame/components/component.dart'; import 'package:flame/components/particle_component.dart'; -import 'package:flame/components/particles/circle_particle.dart'; -import 'package:flame/components/particles/moving_particle.dart'; -import 'package:flame/components/particles/translated_particle.dart'; -import 'package:flame/components/particles/computed_particle.dart'; -import 'package:flame/components/particles/image_particle.dart'; -import 'package:flame/components/particles/rotating_particle.dart'; -import 'package:flame/components/particles/accelerated_particle.dart'; -import 'package:flame/components/particles/paint_particle.dart'; +import 'package:flame/flare_animation.dart'; +import 'package:flame/particles/circle_particle.dart'; +import 'package:flame/particles/composed_particle.dart'; +import 'package:flame/particles/curved_particle.dart'; +import 'package:flame/particles/moving_particle.dart'; +import 'package:flame/particles/sprite_particle.dart'; +import 'package:flame/particles/translated_particle.dart'; +import 'package:flame/particles/computed_particle.dart'; +import 'package:flame/particles/image_particle.dart'; +import 'package:flame/particles/rotating_particle.dart'; +import 'package:flame/particles/accelerated_particle.dart'; +import 'package:flame/particles/paint_particle.dart'; +import 'package:flame/particles/animation_particle.dart'; +import 'package:flame/particles/component_particle.dart'; +import 'package:flame/particles/flare_particle.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; +import 'package:flame/time.dart' as flame_time; +import 'package:flame/particle.dart'; import 'package:flame/position.dart'; +import 'package:flame/sprite.dart'; +import 'package:flame/spritesheet.dart'; import 'package:flame/text_config.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Animation, Image; -void main() async { - Size gameSize; - WidgetsFlutterBinding.ensureInitialized(); - - await Future.wait([ - Flame.util.initialDimensions().then((size) => gameSize = size), - Flame.images.loadAll(const ['zap.png']), - ]); - - final game = MyGame(gameSize); - runApp(game.widget); -} +void main() async => runApp((await loadGame()).widget); class MyGame extends BaseGame { /// Defines dimensions of the sample @@ -40,20 +43,28 @@ class MyGame extends BaseGame { /// by examples below final Random rnd = Random(); final StepTween steppedTween = StepTween(begin: 0, end: 5); + final trafficLight = TrafficLightComponent(); final TextConfig fpsTextConfig = const TextConfig( color: const Color(0xFFFFFFFF), ); + /// Defines the lifespan of all the particles in these examples + final sceneDuration = const Duration(seconds: 1); + Offset cellSize; Offset halfCellSize; + FlareAnimation flareAnimation; - MyGame(Size screenSize) { + MyGame({ + Size screenSize, + this.flareAnimation, + }) { size = screenSize; cellSize = Offset(size.width / gridSize, size.height / gridSize); halfCellSize = cellSize * .5; // Spawn new particles every second - Timer.periodic(const Duration(seconds: 1), (_) => spawnParticles()); + Timer.periodic(sceneDuration, (_) => spawnParticles()); } /// Showcases various different uses of [Particle] @@ -70,6 +81,7 @@ class MyGame extends BaseGame { easedMovingParticle(), intervalMovingParticle(), computedParticle(), + chainingBehaviors(), steppedComputedParticle(), reuseParticles(), imageParticle(), @@ -77,6 +89,11 @@ class MyGame extends BaseGame { rotatingImage(), acceleratedParticles(), paintParticle(), + spriteParticle(), + animationParticle(), + fireworkParticle(), + componentParticle(), + flareParticle(), ]; // Place all the [Particle] instances @@ -90,27 +107,19 @@ class MyGame extends BaseGame { cellSize.scale(col.toDouble(), row.toDouble()) + (cellSize * .5); add( - TranslatedParticle( - lifespan: 1.0, - offset: cellCenter, - child: particle, + // Bind all the particles to a [Component] update + // lifecycle from the [BaseGame]. + ParticleComponent( + particle: TranslatedParticle( + duration: sceneDuration, + offset: cellCenter, + child: particle, + ), ), ); } while (particles.isNotEmpty); } - /// Returns random [Offset] within a virtual - /// grid cell - Offset randomCellOffset() { - return cellSize.scale(rnd.nextDouble(), rnd.nextDouble()) - halfCellSize; - } - - /// Returns random [Color] from primary swatches - /// of material palette - Color randomMaterialColor() { - return Colors.primaries[rnd.nextInt(Colors.primaries.length)]; - } - /// Simple static circle, doesn't move or /// change any of its attributes Particle circle() { @@ -228,7 +237,6 @@ class MyGame extends BaseGame { /// many [Particle] together and having all the computations in place. Particle computedParticle() { return ComputedParticle( - lifespan: 2, renderer: (canvas, particle) => canvas.drawCircle( Offset.zero, particle.progress * halfCellSize.dx, @@ -338,8 +346,8 @@ class MyGame extends BaseGame { count: 10, generator: (i) => AcceleratedParticle( speed: - Offset(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .4, - acceleration: const Offset(0, 600), + Offset(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .2, + acceleration: const Offset(0, 200), child: rotatingImage(initialAngle: rnd.nextDouble() * pi), ), ); @@ -378,6 +386,129 @@ class MyGame extends BaseGame { ); } + /// [SpriteParticle] allows easily embed + /// Flame's [Sprite] into the effect. + Particle spriteParticle() { + return SpriteParticle( + sprite: Sprite('zap.png'), + size: Position.fromOffset(cellSize * .5), + ); + } + + /// An [AnimationParticle] takes a Flame [Animation] + /// and plays it during the particle lifespan. + Particle animationParticle() { + return AnimationParticle( + animation: getBoomAnimation(), + size: Position(128, 128), + ); + } + + /// [ComponentParticle] proxies particle lifecycle hooks + /// to its child [Component]. In example below, [Component] is + /// reused between particle effects and has internal behavior + /// which is independent from the parent [Particle]. + Particle componentParticle() { + return MovingParticle( + from: -halfCellSize * .2, + to: halfCellSize * .2, + curve: SineCurve(), + child: ComponentParticle(component: trafficLight), + ); + } + + /// Not very realistic firework, yet it highlights + /// use of [ComputedParticle] within other particles, + /// mixing predefined and fully custom behavior. + Particle fireworkParticle() { + // A pallete to paint over the "sky" + final List paints = [ + Colors.amber, + Colors.amberAccent, + Colors.red, + Colors.redAccent, + Colors.yellow, + Colors.yellowAccent, + // Adds a nice "lense" tint + // to overall effect + Colors.blue, + ].map((color) => Paint()..color = color).toList(); + + return Particle.generate( + count: 10, + generator: (i) { + final initialSpeed = randomCellOffset(); + final deceleration = initialSpeed * -1; + const gravity = const Offset(0, 40); + + return AcceleratedParticle( + speed: initialSpeed, + acceleration: deceleration + gravity, + child: ComputedParticle(renderer: (canvas, particle) { + final paint = randomElement(paints); + // Override the color to dynamically update opacity + paint.color = paint.color.withOpacity(1 - particle.progress); + + canvas.drawCircle( + Offset.zero, + // Closer to the end of lifespan particles + // will turn into larger glaring circles + rnd.nextDouble() * particle.progress > .6 + ? rnd.nextDouble() * (50 * particle.progress) + : 2 + (3 * particle.progress), + paint, + ); + }), + ); + }, + ); + } + + /// [FlareParticle] renders fiven [FlareAnimation] inside + /// as you can see, animation could be reused across + /// different particles. + Particle flareParticle() { + final flare = ComposedParticle(children: [ + // Circle Particle for background + CircleParticle( + paint: Paint()..color = Colors.white12, + radius: flareAnimation.width / 2), + FlareParticle(flare: flareAnimation), + ]); + + final List corners = [ + -halfCellSize, + halfCellSize, + ]; + + return RotatingParticle( + to: pi, + child: Particle.generate( + count: 2, + generator: (i) => MovingParticle( + to: corners[i] * .4, + curve: SineCurve(), + child: flare, + ), + ), + ); + } + + Particle chainingBehaviors() { + return Particle.generate( + count: 10, + generator: (i) => CircleParticle( + paint: Paint()..color = randomMaterialColor(), + ) + .translated( + -halfCellSize, + ) + .accelerated( + acceleration: randomCellOffset(), + ), + ); + } + @override bool debugMode() => true; @override @@ -389,6 +520,68 @@ class MyGame extends BaseGame { Position(0, size.height - 24)); } } + + /// Returns random [Offset] within a virtual + /// grid cell + Offset randomCellOffset() { + return cellSize.scale(rnd.nextDouble(), rnd.nextDouble()) - halfCellSize; + } + + /// Returns random [Color] from primary swatches + /// of material palette + Color randomMaterialColor() { + return Colors.primaries[rnd.nextInt(Colors.primaries.length)]; + } + + /// Returns a random element from a given list + T randomElement(List list) { + return list[rnd.nextInt(list.length)]; + } + + /// Sample "explosion" animation for [AnimationParticle] example + Animation getBoomAnimation() { + const columns = 8; + const rows = 8; + const frames = columns * rows; + const imagePath = 'boom3.png'; + final spriteImage = Flame.images.loadedFiles[imagePath]; + final spritesheet = SpriteSheet( + rows: rows, + columns: columns, + imageName: imagePath, + textureWidth: spriteImage.width ~/ columns, + textureHeight: spriteImage.height ~/ rows, + ); + final sprites = List.generate( + frames, + (i) => spritesheet.getSprite(i ~/ rows, i % columns), + ); + + return Animation.spriteList(sprites); + } +} + +Future loadGame() async { + Size gameSize; + WidgetsFlutterBinding.ensureInitialized(); + + await Future.wait([ + Flame.util.initialDimensions().then((size) => gameSize = size), + Flame.images.loadAll(const [ + 'zap.png', + + /// Credits to Stumpy Strust from + /// https://opengameart.org/content/explosion-sheet + 'boom3.png', + ]), + ]); + const flareSize = 32.0; + final flareAnimation = await FlareAnimation.load('assets/diamond.flr'); + flareAnimation.updateAnimation('Spin'); + flareAnimation.width = flareSize; + flareAnimation.height = flareSize; + + return MyGame(screenSize: gameSize, flareAnimation: flareAnimation); } /// A curve which maps sinus output (-1..1,0..pi) @@ -399,3 +592,33 @@ class SineCurve extends Curve { return (sin(pi * (t * 2 - 1 / 2)) + 1) / 2; } } + +/// Sample for [ComponentParticle], changes its colors +/// each 2s of registered lifetime. +class TrafficLightComponent extends Component { + final Rect rect = Rect.fromCenter(center: Offset.zero, height: 32, width: 32); + final flame_time.Timer colorChangeTimer = flame_time.Timer(2, repeat: true); + final colors = [ + Colors.green, + Colors.orange, + Colors.red, + ]; + + TrafficLightComponent() { + colorChangeTimer.start(); + } + + @override + void render(Canvas c) { + c.drawRect(rect, Paint()..color = currentColor); + } + + @override + void update(double dt) { + colorChangeTimer.update(dt); + } + + Color get currentColor { + return colors[(colorChangeTimer.progress * colors.length).toInt()]; + } +} diff --git a/doc/examples/particles/pubspec.yaml b/doc/examples/particles/pubspec.yaml index b3c52770a..7a01c7f49 100644 --- a/doc/examples/particles/pubspec.yaml +++ b/doc/examples/particles/pubspec.yaml @@ -18,4 +18,5 @@ dev_dependencies: flutter: assets: - - assets/images/zap.png + - assets/images/ + - assets/diamond.flr diff --git a/doc/examples/particles/test/widget_test.dart b/doc/examples/particles/test/widget_test.dart deleted file mode 100644 index 7a99cee1c..000000000 --- a/doc/examples/particles/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:particles/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/doc/examples/particles/web/index.html b/doc/examples/particles/web/index.html deleted file mode 100644 index c66ef0f2f..000000000 --- a/doc/examples/particles/web/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - particles - - - - - diff --git a/doc/particles.md b/doc/particles.md new file mode 100644 index 000000000..fb2ac140c --- /dev/null +++ b/doc/particles.md @@ -0,0 +1,393 @@ +# Particles + +Flame offerst a basic, yet robust and extendable particle system. The core concept of this system is `Particle` class, which is very similar in its behavior to the `Component`. + +The most basic usage of `Particle` with `BaseGame` would look as following: + +```dart +import 'package:flame/components/particle_component.dart'; + +// ... + +game.add( + // Wrapping a [Particle] with [ParticleComponent] + // which maps [Component] lifecycle hooks to [Particle] ones + // and embeds a trigger for destroying the component. + ParticleComponent( + particle: CircleParticle() + ) +); +``` + +When using `Particle` with custom `Game` implementation, please ensure that `Particle` `update` and `render` lifecycle hooks are called during each game loop frame. + +Main approaches to implement desired particle effects: +* Composition of existing behaviors +* Use behavior chaining (just a syntaxic sugar over first one) +* Using `ComputedParticle` + +Composition works in a similar fashion to those of Flutter widgets by defining the effect from top to bottom. Chaining allows to express same composition trees more fluently by defining behaviors from bottom to top. Computed particles in their turn fully delegate implementation of the behavior to your code. Any of the approaches, though, could be used in conjunction with existing behaviors, where needed. + +Below you can find an example of a effect showing a burst of circles, accelerating from `(0, 0)` to a random directions using all three approaches defined above. +```dart +Random rnd = Random(); +Function randomOffset = () => Offset( + rnd.nextDouble() * 200 - 100, + rnd.nextDouble() * 200 - 100, +); + +// Composition +// Defining particle effect as set of nested +// behaviors from top to bottom, one within another: +// ParticleComponent +// > ComposedParticle +// > AcceleratedParticle +// > CircleParticle +game.add( + ParticleComponent( + particle: Particle.generate( + count: 10, + generator: (i) => AcceleratedParticle( + acceleration: randomOffset(), + child: CircleParticle( + paint: Paint()..color = Colors.red + ) + ) + ) + ) +); + +// Chaining +// Expresses same behavior structure as above, but with more +// fluent API. Only [Particles] with [SingleChildParticle] mixin could +// be used as chainable behaviors. +game.add( + Particle + .generate( + count: 10, + generator: (i) => CircleParticle(paint: Paint()..color = Colors.red) + .accelerating(randomOffset()) + ) + .component() +); + +// Computed Particle +// All the behavior is defined explicitly. Offers greater flexibility +// compared to built-in behaviors. +game.add( + Particle + .generate( + count: 10, + generator: (i) { + final position = Offset.zero; + final speed = Offset.zero; + final acceleration = randomOffset(); + final paint = Paint()..color = Colors.red; + + return ComputedParticle( + renderer: (canvas, _) { + speed += acceleration; + position += speed; + canvas.drawCircle(position, 10, paint); + } + ); + } + ) + .component() +) +``` + +You can find more examples of using different built-int particles in various combinations [here](/docs/examples/particles/lib/main.dart). + +## Lifecycle + +Behavior common to all `Particle`s is that all of them accept either `lifespan` or `duration`. These values are used to make `ParticleCoponent` self-destoy, once its internal `Particle` has reached the end of its life. Time within the `Particle` itself is tracked using the Flame `Timer`. It could be configured with either `double`, representing seconds (with microsecond precision), or with Dart built-in: `Duration` by passing _either_ of these into corresponding `Particle` constructor. + +```dart +Particle(lifespan: .2); // will live for 200ms +Particle(lifespan: 4); // will live for 4s +Particle(duration: const Duration(milliseconds: 169)); // will live for 169ms +``` + +It is also possible to reset `Particle` lifespan by using `setLifespan` method, which accepts a `double` of seconds. + +```dart +final particle = Particle(lifespan: 2); + +// ... at some point of time later +particle.setLifespan(2) // will live for another 2s from this moment +``` + +During its lifetime, `Particle` tracks the time it was alive and exposes it with `progress` getter, which is a unit double, which values are always spanning from 0 to 1. Its value could be used in a similar fashion as `value` of `AnimationController` in Flutter. + +```dart +final duration = const Duration(seconds: 2); +final particle = Particle(duration: const Duration(seconds)); + +game.add(ParticleComponent(particle: particle)); + +// Will print values from 0 to 1 with step of .1: 0, 0.1, 0.2 ... 0.9, 1.0 +Timer.periodic(duration * .1, () => print(particle.progress)); +``` +Lifespan is passed down to all the descendants of given `Particle` if it supports any of the nesting behaviors. + +## Built-in particles + +Flame ships with a few built-in `Particle` behaviors: +* The `TranslatedParticle`, translates its `child` by given `Offset` +* The `MovingParticle`, moves its `child` between two predefined `Offset`, supports `Curve` +* The `AcceleratedParticle`, allows basic physics based effects, like gravitation or speed dampening +* The `CircleParticle`, renders circles of all shapes and sizes +* The `SpriteParticle`, renders Flame `Sprite` within a `Particle` effect +* The `ImageParticle`, renders *dart:ui* `Image` within a `Particle` effect +* The `ComponentParticle`, renders Flame `Component` within a `Particle` effect +* The `FlareParticle`, renders Flare animation within a `Particle` effect + +## Translated Particle + +Simply translates underlying `Particle` to a specified `Offset` within the rendering `Canvas`. +Does not change or alter its position, consider using `MovingParticle` or `AcceleratedParticle` where change of position is required. +Same effect could be achieved by translating `Canvas` layer. + +```dart +game.add( + ParticleComponent( + particle: TranslatedParticle( + // Will translate child Particle effect to + // the center of game canvas + offset: game.size.center(Offset.zero), + child: Particle(), + ) + ) +); +``` + +## Moving Particle + +Moves child `Particle` between `from` and `to` `Offset`s during its lifespan. Supports `Curve` via `CurvedParticle`. + +```dart +game.add( + ParticleComponent( + particle: MovingParticle( + // Will move from corner to corner of the + // game canvas + from: game.size.topLeft(Offset.zero), + to: game.size.bottomRight(Offset.zero), + child: Particle(), + ) + ) +); +``` + +## Accelerated Particle + +A basic physics particle which allows you to specify its initial `position`, `speed` and `acceleration` and let `update` cycle do the rest. All three specified as `Offset`s, +which you can think of as vectors. Works especially well for physics-based "bursts", but not limited to that. +Unit of the `Offset` value is _logical px/s_. So a speed of `Offset(0, 100)` will move a child `Particle` by 100 logical pixels of the device every second of game time. + +```dart +final rnd = Random(); +game.add( + ParticleComponent( + particle: AcceleratedParticle( + // Will fire off in the center of game canvas + position: game.size.center(Offset.zero), + // With random initial speed of Offset(-100..100, 0..-100) + speed: Offset(rnd.nextDouble() * 200 - 100, -rnd.nextDouble() * 100), + // Accelerating downwards, simulating "gravity" + speed: Offset(0, 100), + child: Particle(), + ) + ) +); +``` + +## Circle Particle + +A `Particle` which renders circle with given `Paint` at the zero offset of passed `Canvas`. Use in conjunction with `TranslatedParticle`, `MovingParticle` or `AcceleratedParticle` +in order to achieve desired positioning. + +```dart +game.add( + ParticleComponent( + particle: CircleParticle( + radius: game.size.width / 2, + paint: Paint()..color = Colors.red.withOpacity(.5), + ) + ) +); +``` + +## Sprite Particle +Allows you to embed Flame's `Sprite` into your particle effects. Useful when consuming graphics for the effect from `SpriteSheet`. +```dart +game.add( + ParticleComponent( + particle: SpriteParticle( + sprite: Sprite('sprite.png'), + size: Position(64, 64), + ) + ) +); +``` + +## Image Particle +Renders given `dart:ui` image within the particle tree. +```dart +// During game initialisation +await Flame.images.loadAll(const [ + 'image.png' +]); + +// ... + +// Somewhere during the game loop +game.add( + ParticleComponent( + particle: ImageParticle( + size: const Size.square(24), + image: Flame.images.loadedFiles['image.png'], + ); + ) +); +``` + +## Animation Particle +A `Particle` which embeds Flame `Animation`. By default, aligns `Animation`s `stepTime` so that it's fully played during `Particle` lifespan. It's possible to override this behavior with `alignAnimationTime` parameter. + +```dart +final spritesheet = SpriteSheet( + imageName: 'spritesheet.png', + textureWidth: 16, + textureHeight: 16, + columns: 10, + rows: 2 +); + +game.add( + ParticleComponent( + particle: AnimationParticle( + animation: spritesheet.createAnimation(0, stepTime: 0.1), + ); + ) +); +``` + +## Component Particle +This `Particle` allows you to embed Flame `Component` within the particle effects. `Component` could have it's own `update` lifecycle and +could be reused across different effect trees. If the only thing you need is to add some dynamics to an instance of certain `Component`, please consider +adding it to the `game` directly, without the `Particle` in the middle. + +```dart +var longLivingRect = RectComponent(); + +game.add( + ParticleComponent( + particle: ComponentParticle( + component: longLivingRect + ); + ) +); + +class RectComponent extends Component { + void render(Canvas c) { + c.drawRect( + Rect.fromCenter(center: Offset.zero, width: 100, height: 100), + Paint()..color = Colors.red + ); + } + + void update(double dt) { + /// Will be called by parent [Particle] + } +} +``` + +## Flare Particle +A container for `FlareAnimation`, propagates `update` and `render` hooks to its child. + +```dart +// During game initialisation +const flareSize = 32.0; +final flareAnimation = await FlareAnimation.load('assets/sparkle.flr'); +flareAnimation.updateAnimation('Shine'); +flareAnimation.width = flareSize; +flareAnimation.height = flareSize; + +// Somewhere in game +game.add( + ParticleComponent( + particle: FlareParticle(flare: flareAnimation); + ) +); +``` + +## Computed Particle +A `Particle` which could help you when: +* Default behavior is not enough +* Complex effects optimization +* Custom easings + +When created, delegates all the rendering to a supplied `ParticleRenderDelegate` which is called on each frame +to perform necessary computations and render something to the `Canvas` + +```dart +game.add( + ParticleComponent( + // Renders a circle which gradually + // changes its color and size during the particle lifespan + particle: ComputedParticle( + renderer: (canvas, particle) => canvas.drawCircle( + Offset.zero, + particle.progress * 10, + Paint() + ..color = Color.lerp( + Colors.red, + Colors.blue, + particle.progress, + ), + ), + ) + ) +) +``` + +## Nesting behavior + +Flame's implementation of particles follows same pattern of extreme composition as Flutter widgets. That +is achieved by incapsulating small pieces of behavior in every of particles and then nesting these behaviors together to achieve desired visual effect. + +Two entities allowing `Particle` to nest each other are: `SingleChildParticle` mixin and `ComposedParticle` class. + +`SingleChildParticle` may help you with creating `Particles` with a custom behavior, +for example, randomly positioning it's child during each frame: +```dart +var rnd = Random(); +class GlitchParticle extends Particle with SingleChildParticle { + @override + Particle child; + + GlitchParticle({ + @required this.child, + double lifespan, + Duration duration, + }) : super( + lifespan: lifespan, + duration: duration, + ); + + @override + render(Canvas canvas) { + canvas.save(); + canvas.translate(rnd.nextDouble() * 100, rnd.nextDouble() * 100); + + // Will also render the child + super.render(); + + canvas.restore(); + } +} +``` + +`ComposedParticle` could be used either as standalone or within existing `Particle` tree. \ No newline at end of file diff --git a/lib/components/mixins/single_child_particle.dart b/lib/components/mixins/single_child_particle.dart index f4819fb67..3fa6101bb 100644 --- a/lib/components/mixins/single_child_particle.dart +++ b/lib/components/mixins/single_child_particle.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import '../particle_component.dart'; +import '../../particle.dart'; /// Implements basic behavior for nesting [Particle] instances /// into each other. @@ -27,17 +27,23 @@ mixin SingleChildParticle on Particle { @override void setLifespan(double lifespan) { + assert(child != null); + super.setLifespan(lifespan); child.setLifespan(lifespan); } @override void render(Canvas c) { + assert(child != null); + child.render(c); } @override void update(double t) { + assert(child != null); + super.update(t); child.update(t); } diff --git a/lib/components/particle_component.dart b/lib/components/particle_component.dart index 0c064d1cf..be2ff87e9 100644 --- a/lib/components/particle_component.dart +++ b/lib/components/particle_component.dart @@ -1,79 +1,37 @@ import 'dart:ui'; -import 'package:flame/components/particles/composed_particle.dart'; import 'package:flutter/foundation.dart'; -import '../time.dart'; +import '../particle.dart'; import 'component.dart'; -/// A function which returns [Particle] when called -typedef ParticleGenerator = Particle Function(int); +/// Base container for [Particle] instances to be attach +/// to a [Component] tree. Could be added either to [BaseGame] +/// or [ComposedComponent] as needed. +/// Proxies [Component] lifecycle hooks to nested [Particle]. +class ParticleComponent extends Component { + Particle particle; -/// Base class implementing common behavior for all the particles. -/// -/// Intention is to follow same "Extreme Composability" style -/// as across the whole Flutter framework, so each type of particle implements -/// some particular behavior which then could be nested and combined together -/// to create specifically required experience. -abstract class Particle extends Component { - /// Generates given amount of particles, - /// combining them into one [ComposedParticle] - /// Useful for procedural particle generation. - static Particle generate({ - int count = 10, - @required ParticleGenerator generator, - double lifespan, - Duration duration, - }) { - return ComposedParticle( - lifespan: lifespan, - duration: duration, - children: List.generate(count, generator), - ); - } - - Timer _timer; - bool _shouldBeDestroyed = false; - - Particle({ - /// Particle lifespan in [Timer] format, - /// double in seconds with microsecond precision - double lifespan, - - /// Another way to set lifespan, using Flutter - /// [Duration] class - Duration duration, - }) { - /// Either [double] lifespan or [Duration] duration, - /// defaulting to 500 milliseconds of life (or .5, in [Timer] double) - lifespan = lifespan ?? - (duration ?? const Duration(milliseconds: 500)).inMicroseconds / - Duration.microsecondsPerSecond; - - setLifespan(lifespan); - } + ParticleComponent({ + @required this.particle, + }); + /// This [Component] will be automatically destroyed + /// as soon as @override - bool destroy() => _shouldBeDestroyed; - double get progress => _timer.progress; + bool destroy() => particle.destroy(); + double get progress => particle.progress; + /// Passes rendering chain down to the inset + /// [Particle] within this [Component]. @override void render(Canvas canvas) { - // Do nothing by default + particle.render(canvas); } + /// Passes update chain to child [Particle]. @override void update(double dt) { - _timer.update(dt); - - if (_timer.progress >= 1) { - _shouldBeDestroyed = true; - } - } - - void setLifespan(double lifespan) { - _timer?.stop(); - _timer = Timer(lifespan); - _timer.start(); + particle.update(dt); } } diff --git a/lib/particle.dart b/lib/particle.dart new file mode 100644 index 000000000..c34bd62e8 --- /dev/null +++ b/lib/particle.dart @@ -0,0 +1,166 @@ +import 'dart:ui'; + +import 'package:flame/components/particle_component.dart'; +import 'package:flame/particles/accelerated_particle.dart'; +import 'package:flame/particles/moving_particle.dart'; +import 'package:flame/particles/translated_particle.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; + +import 'components/component.dart'; +import 'particles/composed_particle.dart'; +import 'time.dart'; + +/// A function which returns [Particle] when called +typedef ParticleGenerator = Particle Function(int); + +/// Base class implementing common behavior for all the particles. +/// +/// Intention is to follow same "Extreme Composability" style +/// as across the whole Flutter framework, so each type of particle implements +/// some particular behavior which then could be nested and combined together +/// to create specifically required experience. +abstract class Particle { + /// Generates given amount of particles, + /// combining them into one [ComposedParticle] + /// Useful for procedural particle generation. + static Particle generate({ + int count = 10, + @required ParticleGenerator generator, + double lifespan, + Duration duration, + }) { + return ComposedParticle( + lifespan: lifespan, + duration: duration, + children: List.generate(count, generator), + ); + } + + /// Internal timer defining how long + /// this [Particle] will live. [Particle] will + /// be marked for destroy when this timer is over. + Timer _timer; + + /// Stores desired lifespan of the + /// particle in seconds. + double _lifespan; + + /// Will be set to true by update hook + /// when this [Particle] reaches end of its lifespan + bool _shouldBeDestroyed = false; + + Particle({ + /// Particle lifespan in [Timer] format, + /// double in seconds with microsecond precision + double lifespan, + + /// Another way to set lifespan, using Flutter + /// [Duration] class + Duration duration, + }) { + /// Either [double] lifespan or [Duration] duration, + /// defaulting to 500 milliseconds of life (or .5, in [Timer] double) + lifespan = lifespan ?? + (duration ?? const Duration(milliseconds: 500)).inMicroseconds / + Duration.microsecondsPerSecond; + + setLifespan(lifespan); + } + + /// This method will return true as + /// soon as particle reaches an end of its + /// lifespan, which means it's ready to be + /// destroyed by a wrapping container. + /// Follows same style as [Component]. + bool destroy() => _shouldBeDestroyed; + + /// Getter which should be used by subclasses + /// to get overall progress. Also allows to substitute + /// progres with other values, for example adding easing as in [CurvedParticle]. + double get progress => _timer.progress; + + /// Should render this [Particle] to given [Canvas]. + /// Default behavior is empty, so that it's not + /// required to override this in [Particle] which + /// render nothing and serve as behavior containers. + void render(Canvas canvas) { + // Do nothing by default + } + + /// Updates internal [Timer] of this [Particle] + /// which defines its position on the lifespan. + /// Marks [Particle] for destroy when it is over. + void update(double dt) { + _timer.update(dt); + + if (_timer.progress >= 1) { + _shouldBeDestroyed = true; + } + } + + /// A control method allowing a parent of this [Particle] + /// to pass down it's lifespan. Allows to only specify desired lifespan + /// once, at the very top of the [Particle] tree which + /// then will be propagated down using this method. + /// See [SingleChildParticle] or [ComposedParticle] for details. + void setLifespan(double lifespan) { + _lifespan = lifespan; + _timer?.stop(); + _timer = Timer(lifespan); + _timer.start(); + } + + /// Wtaps this particle with [TranslatedParticle] + /// statically repositioning it for the time + /// of the lifespan. + Particle translated(Offset offset) { + return TranslatedParticle( + offset: offset, + child: this, + lifespan: _lifespan, + ); + } + + /// Wraps this particle with [MovingParticle] + /// allowing it to move from one [Offset] + /// on the canvas to another one. + Particle moving({ + Offset from = Offset.zero, + @required Offset to, + Curve curve = Curves.linear, + }) { + return MovingParticle( + from: from, + to: to, + curve: curve, + child: this, + lifespan: _lifespan, + ); + } + + /// Wraps this particle with [AcceleratedParticle] + /// allowing to specify desired position speed and acceleration + /// and leave the basic physics do the rest. + Particle accelerated({ + Offset acceleration = Offset.zero, + Offset position = Offset.zero, + Offset speed = Offset.zero, + }) { + return AcceleratedParticle( + position: position, + speed: speed, + acceleration: acceleration, + child: this, + lifespan: _lifespan, + ); + } + + /// Wraps this particle with [ParticleComponent] + /// to be used within the [BaseGame] component system. + Component component() { + return ParticleComponent( + particle: this + ); + } +} diff --git a/lib/components/particles/accelerated_particle.dart b/lib/particles/accelerated_particle.dart similarity index 85% rename from lib/components/particles/accelerated_particle.dart rename to lib/particles/accelerated_particle.dart index 144b35551..639761334 100644 --- a/lib/components/particles/accelerated_particle.dart +++ b/lib/particles/accelerated_particle.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../mixins/single_child_particle.dart'; -import '../particle_component.dart'; +import '../components/mixins/single_child_particle.dart'; +import '../particle.dart'; import 'curved_particle.dart'; /// A particle serves as a container for basic @@ -26,7 +26,10 @@ class AcceleratedParticle extends CurvedParticle with SingleChildParticle { this.position = Offset.zero, double lifespan, Duration duration, - }) : super(lifespan: lifespan, duration: duration); + }) : super( + lifespan: lifespan, + duration: duration, + ); @override void render(Canvas canvas) { diff --git a/lib/particles/animation_particle.dart b/lib/particles/animation_particle.dart new file mode 100644 index 000000000..600175e8d --- /dev/null +++ b/lib/particles/animation_particle.dart @@ -0,0 +1,50 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../animation.dart'; +import '../particle.dart'; +import '../position.dart'; + +class AnimationParticle extends Particle { + final Animation animation; + final Position size; + final Paint overridePaint; + final bool alignAnimationTime; + + AnimationParticle({ + @required this.animation, + this.size, + this.overridePaint, + double lifespan, + Duration duration, + this.alignAnimationTime = true, + }) : super( + lifespan: lifespan, + duration: duration, + ); + + @override + void setLifespan(double lifespan) { + super.setLifespan(lifespan); + + if (alignAnimationTime) { + animation.stepTime = lifespan / animation.frames.length; + animation.reset(); + } + } + + @override + void render(Canvas canvas) { + animation.getSprite().renderCentered(canvas, Position.empty(), + overridePaint: overridePaint, + size: size + ); + } + + @override + void update(double dt) { + super.update(dt); + animation.update(dt); + } +} \ No newline at end of file diff --git a/lib/components/particles/circle_particle.dart b/lib/particles/circle_particle.dart similarity index 89% rename from lib/components/particles/circle_particle.dart rename to lib/particles/circle_particle.dart index 10eb13aa5..c13e485b9 100644 --- a/lib/components/particles/circle_particle.dart +++ b/lib/particles/circle_particle.dart @@ -2,8 +2,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../../time.dart'; -import '../particle_component.dart'; +import '../particle.dart'; /// Plain circle with no other behaviors /// Consider composing with other [Particle] diff --git a/lib/particles/component_particle.dart b/lib/particles/component_particle.dart new file mode 100644 index 000000000..2f5296f36 --- /dev/null +++ b/lib/particles/component_particle.dart @@ -0,0 +1,35 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../particle.dart'; +import '../position.dart'; +import '../components/component.dart'; + +class ComponentParticle extends Particle { + final Component component; + final Position size; + final Paint overridePaint; + + ComponentParticle({ + @required this.component, + this.size, + this.overridePaint, + double lifespan, + Duration duration, + }) : super( + lifespan: lifespan, + duration: duration, + ); + + @override + void render(Canvas canvas) { + component.render(canvas); + } + + @override + void update(double dt) { + super.update(dt); + component.update(dt); + } +} \ No newline at end of file diff --git a/lib/components/particles/composed_particle.dart b/lib/particles/composed_particle.dart similarity index 95% rename from lib/components/particles/composed_particle.dart rename to lib/particles/composed_particle.dart index 80135d4c4..40b987a92 100644 --- a/lib/components/particles/composed_particle.dart +++ b/lib/particles/composed_particle.dart @@ -1,9 +1,8 @@ import 'dart:ui'; +import 'package:flame/particle.dart'; import 'package:flutter/foundation.dart'; -import '../particle_component.dart'; - /// A single [Particle] which manages multiple children /// by proxying all lifecycle hooks. class ComposedParticle extends Particle { diff --git a/lib/components/particles/computed_particle.dart b/lib/particles/computed_particle.dart similarity index 95% rename from lib/components/particles/computed_particle.dart rename to lib/particles/computed_particle.dart index d39ec5d56..badf70d08 100644 --- a/lib/components/particles/computed_particle.dart +++ b/lib/particles/computed_particle.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../particle_component.dart'; +import '../particle.dart'; /// A function which should render desired contents /// onto a given canvas. External state needed for diff --git a/lib/components/particles/curved_particle.dart b/lib/particles/curved_particle.dart similarity index 77% rename from lib/components/particles/curved_particle.dart rename to lib/particles/curved_particle.dart index 9e8aee452..c24a2e741 100644 --- a/lib/components/particles/curved_particle.dart +++ b/lib/particles/curved_particle.dart @@ -1,6 +1,6 @@ import 'package:flutter/animation.dart'; -import '../particle_component.dart'; +import '../particle.dart'; /// A [Particle] which applies certain [Curve] for /// easing or other purposes to its [progress] getter. @@ -11,7 +11,10 @@ class CurvedParticle extends Particle { this.curve = Curves.linear, double lifespan, Duration duration, - }) : super(lifespan: lifespan, duration: duration); + }) : super( + lifespan: lifespan, + duration: duration, + ); @override double get progress => curve.transform(super.progress); diff --git a/lib/particles/flare_particle.dart b/lib/particles/flare_particle.dart new file mode 100644 index 000000000..792ad85fe --- /dev/null +++ b/lib/particles/flare_particle.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../flare_animation.dart'; +import '../particle.dart'; + +class FlareParticle extends Particle { + final FlareAnimation flare; + + FlareParticle({ + @required this.flare, + double lifespan, + Duration duration, + }) : super( + lifespan: lifespan, + duration: duration, + ); + + @override + void render(Canvas canvas) { + canvas.save(); + canvas.translate(-flare.width / 2, -flare.height / 2); + flare.render(canvas); + canvas.restore(); + } + + @override + void update(double dt) { + super.update(dt); + flare.update(dt); + } +} \ No newline at end of file diff --git a/lib/components/particles/image_particle.dart b/lib/particles/image_particle.dart similarity index 96% rename from lib/components/particles/image_particle.dart rename to lib/particles/image_particle.dart index 1eeb2688b..5964d1328 100644 --- a/lib/components/particles/image_particle.dart +++ b/lib/particles/image_particle.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../particle_component.dart'; +import '../particle.dart'; /// A [Particle] which renders given [Image] on a [Canvas] /// image is centered. If any other behavior is needed, consider diff --git a/lib/components/particles/moving_particle.dart b/lib/particles/moving_particle.dart similarity index 91% rename from lib/components/particles/moving_particle.dart rename to lib/particles/moving_particle.dart index 37d630422..ca820f4db 100644 --- a/lib/components/particles/moving_particle.dart +++ b/lib/particles/moving_particle.dart @@ -3,9 +3,9 @@ import 'dart:ui'; import 'package:flutter/animation.dart'; import 'package:flutter/foundation.dart'; +import '../particle.dart'; +import '../components/mixins/single_child_particle.dart'; import '../particles/curved_particle.dart'; -import '../mixins/single_child_particle.dart'; -import '../particle_component.dart'; /// Statically offset given child [Particle] by given [Offset] /// If you're loking to move the child, consider [MovingParticle] diff --git a/lib/components/particles/paint_particle.dart b/lib/particles/paint_particle.dart similarity index 92% rename from lib/components/particles/paint_particle.dart rename to lib/particles/paint_particle.dart index c570f4723..3b7e54529 100644 --- a/lib/components/particles/paint_particle.dart +++ b/lib/particles/paint_particle.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../mixins/single_child_particle.dart'; -import '../particle_component.dart'; +import '../components/mixins/single_child_particle.dart'; +import '../particle.dart'; import 'curved_particle.dart'; /// A particle which renders its child with certain [Paint] diff --git a/lib/components/particles/rotating_particle.dart b/lib/particles/rotating_particle.dart similarity index 89% rename from lib/components/particles/rotating_particle.dart rename to lib/particles/rotating_particle.dart index 76bd931e3..798e8e707 100644 --- a/lib/components/particles/rotating_particle.dart +++ b/lib/particles/rotating_particle.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; -import '../mixins/single_child_particle.dart'; -import '../particle_component.dart'; +import '../components/mixins/single_child_particle.dart'; +import '../particle.dart'; import 'curved_particle.dart'; /// A particle which rotates its child over the lifespan diff --git a/lib/particles/sprite_particle.dart b/lib/particles/sprite_particle.dart new file mode 100644 index 000000000..8b83bbab6 --- /dev/null +++ b/lib/particles/sprite_particle.dart @@ -0,0 +1,32 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../particle.dart'; +import '../position.dart'; +import '../sprite.dart'; + +class SpriteParticle extends Particle { + final Sprite sprite; + final Position size; + final Paint overridePaint; + + SpriteParticle({ + @required this.sprite, + this.size, + this.overridePaint, + double lifespan, + Duration duration, + }) : super( + lifespan: lifespan, + duration: duration, + ); + + @override + void render(Canvas canvas) { + sprite.renderCentered(canvas, Position.empty(), + overridePaint: overridePaint, + size: size + ); + } +} \ No newline at end of file diff --git a/lib/components/particles/translated_particle.dart b/lib/particles/translated_particle.dart similarity index 83% rename from lib/components/particles/translated_particle.dart rename to lib/particles/translated_particle.dart index 8593975c3..9c16d5959 100644 --- a/lib/components/particles/translated_particle.dart +++ b/lib/particles/translated_particle.dart @@ -1,10 +1,9 @@ import 'dart:ui'; -import 'package:flame/components/mixins/single_child_particle.dart'; import 'package:flutter/foundation.dart'; -import '../../time.dart'; -import '../particle_component.dart'; +import '../components/mixins/single_child_particle.dart'; +import '../particle.dart'; /// Statically offset given child [Particle] by given [Offset] /// If you're loking to move the child, consider [MovingParticle]