Migrated the Particle API to Vector2 (#728)

* Migrated the `Particle` API to `Vector2`

* Update doc/particles.md

Co-authored-by: Erick <erickzanardoo@gmail.com>

* Follow-up on Erick's review

* Fix

* Apply suggestions from code review

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>

* Update

* Update

* Update

* Update packages/flame/lib/src/extensions/vector2.dart

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>

* Updated

* Update

* Using scale

Co-authored-by: Erick <erickzanardoo@gmail.com>
Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
Jochum van der Ploeg
2021-05-27 21:40:50 +02:00
committed by GitHub
parent 609ab27d69
commit 021b453b54
10 changed files with 285 additions and 282 deletions

View File

@ -6,27 +6,27 @@ the `Particle` class, which is very similar in its behavior to the `ParticleComp
The most basic usage of a `Particle` with `BaseGame` would look as following:
```dart
import 'package:flame/components/particle_component.dart';
import 'package:flame/components.dart';
// ...
game.add(
// Wrapping a [Particle] with [ParticleComponent]
// which maps the [Component] lifecycle hooks to the [Particle] ones
// and embeds a trigger for removing the component.
ParticleComponent(
particle: CircleParticle()
)
// Wrapping a Particle with ParticleComponent
// which maps Component lifecycle hooks to Particle ones
// and embeds a trigger for removing the component.
ParticleComponent(
particle: CircleParticle(),
),
);
```
When using `Particle` with a custom `Game` implementation, please ensure that the `Particle`'s
`update` and `render` lifecycle hooks are called during each game loop frame.
When using `Particle` with a custom `Game` implementation, please ensure that both the `update` and
`render` methods are called during each game loop tick.
The main approaches to implement the desired particle effects are:
- Composition of existing behaviors
- Use behavior chaining (just a syntactic sugar of the first one)
- Using `ComputedParticle`
Main approaches to implement desired particle effects:
* Composition of existing behaviors.
* Use behavior chaining (just a syntactic sugar of the 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 the same composition trees more fluently by defining behaviors
@ -34,15 +34,14 @@ from bottom to top. Computed particles in their turn fully delegate implementati
to your code. Any of the approaches could be used in conjunction with existing behaviors where
needed.
Below you can find an example of an effect that shows a burst of circles, accelerating from `(0, 0)`
towards random directions using all three approaches defined above.
```dart
Random rng = Random();
Vector2 randomVector2() => (Vector2.random(rng) - Vector2.random(rng)) * 100;
Random rnd = Random();
// Composition
// Defining particle effect as a set of nested behaviors from top to bottom, one within another:
Vector2 randomVector2() => (Vector2.random(rnd) - Vector2.random(rnd)) * 200;
// Composition.
//
// Defining a particle effect as a set of nested behaviors from top to bottom, one within another:
// ParticleComponent
// > ComposedParticle
// > AcceleratedParticle
@ -55,31 +54,30 @@ game.add(
acceleration: randomVector2(),
child: CircleParticle(
paint: Paint()..color = Colors.red,
)
)
)
)
),
),
),
),
);
// Chaining
// Chaining.
//
// Expresses the same behavior as above, but with a more fluent API.
// Only [Particles] with [SingleChildParticle] mixin can be used as chainable behaviors.
// Only Particles with SingleChildParticle mixin can be used as chainable behaviors.
game.add(
Particle
.generate(
count: 10,
generator: (i) {
return CircleParticle(
paint: Paint()..color = Colors.red,
).accelerating(randomVector2())
},
)
.asComponent()
generator: (i) => CircleParticle(paint: Paint()..color = Colors.red)
.accelerating(randomVector2())
)
.asComponent(),
);
// Computed Particle
// All the behaviors are defined explicitly. Offers greater flexibility compared to built-in
// behaviors.
// Computed Particle.
//
// All the behaviors are defined explicitly. Offers greater flexibility
// compared to built-in behaviors.
game.add(
Particle
.generate(
@ -90,12 +88,12 @@ game.add(
final acceleration = randomVector2();
final paint = Paint()..color = Colors.red;
return ComputedParticle(
return ComputedParticle(
renderer: (canvas, _) {
speed += acceleration;
position += speed;
canvas.drawCircle(position, 10, paint);
},
}
);
}
)
@ -103,114 +101,113 @@ game.add(
)
```
You can find more examples of using different built-in particles in various combinations
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/particles).
You can find more examples of how to use different built-in particles in various combinations
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/utils/particles.dart).
## Lifecycle
Behavior common to all `Particle`s is that all of them accept a `lifespan` parameter. This value is
used to make the `ParticleComponent` self-remove, once its internal `Particle` has reached the end
of its life. The time within the `Particle` itself is tracked using a Flame `Timer`. It could be
configured with a `double`, representing seconds (with microsecond precision) by passing it into the
corresponding `Particle` constructor.
A behavior common to all `Particle`s is that all of them accept a `lifespan` argument. This value is
used to make the `ParticleComponent` remove itself once its internal `Particle` has reached the end
of its life. Time within the `Particle` itself is tracked using the Flame `Timer` class. It can be
configured with a `double`, represented in seconds (with microsecond precision) by passing it into
the corresponding `Particle` constructor.
```dart
Particle(lifespan: .2); // will live for 200ms
Particle(lifespan: 4); // will live for 4s
Particle(lifespan: .2); // will live for 200ms.
Particle(lifespan: 4); // will live for 4s.
```
It is also possible to reset the `Particle` lifespan by using `setLifespan` method, which accepts a
`double` that represents seconds.
It is also possible to reset a `Particle`'s lifespan by using the `setLifespan` method, which also
accepts a `double` of seconds.
```dart
final particle = Particle(lifespan: 2);
// ... at some point in time later
particle.setLifespan(2) // will live for another 2s from this moment
// ... after some time.
particle.setLifespan(2) // will live for another 2s.
```
During its lifetime, the `Particle` tracks the time it was alive and exposes it with a `progress`
getter, which is spanning between 0.0 to 1.0. Its value could be used in a similar fashion as
`value` of `AnimationController` in Flutter.
During its lifetime, a `Particle` tracks the time it was alive and exposes it through the `progress`
getter, which returns a value between `0.0` and `1.0`. This value can be used in a similar fashion
as the `value` property of the `AnimationController` class in Flutter.
```dart
final particle = Particle(lifespan: 2.0);
game.add(ParticleComponent(particle: particle));
// Will print values from 0 to 1 with a step length of .1: 0, 0.1, 0.2 ... 0.9, 1.0
// 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));
```
The lifespan is passed down to all the descendants of a given `Particle`, if it supports any of the
nesting behaviors.
The `lifespan` is passed down to all the descendants of a 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 the given `Vector2`
* The `MovingParticle`, moves its `child` between two predefined `Vector2`, it 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 a `Sprite` within a `Particle` effect
* The `ImageParticle`, renders a *dart:ui* `Image` within a `Particle` effect
* The `ComponentParticle`, renders a Flame `Component` within a `Particle` effect
* The `FlareParticle`, renders a Flare animation within a `Particle` effect
* The `TranslatedParticle` translates its `child` by given `Vector2`
* The `MovingParticle` moves its `child` between two predefined `Vector2`, 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
More examples of how to use these behaviors together are available
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/particles).
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/utils/particles.dart).
All the implementations are available in the
[particles](https://github.com/flame-engine/flame/tree/main/packages/flame/lib/src/particles)
folder in the Flame source.
## TranslatedParticle
[particles](https://github.com/flame-engine/flame/tree/main/packages/flame/lib/src/particles) folder
on the Flame repository.
Simply translates the underlying `Particle` to a specified `Vector2` 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 the `Canvas` layer.
Simply translates the underlying `Particle` to a specified `Vector2` 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 the `Canvas`
layer.
```dart
game.add(
ParticleComponent(
particle: TranslatedParticle(
// Will translate child Particle effect to
// the center of game canvas
position: game.size / 2,
// Will translate the child Particle effect to the center of game canvas.
offset: game.size / 2,
child: Particle(),
)
)
),
),
);
```
## MovingParticle
Moves the child `Particle` between `from` and `to` `Vector2`s during its lifespan. Supports `Curve`
via `CurvedParticle`.
Moves the child `Particle` between the `from` and `to` `Vector2`s during its lifespan. Supports
`Curve` via `CurvedParticle`.
```dart
game.add(
ParticleComponent(
particle: MovingParticle(
// Will move from corner to corner of the game canvas
// Will move from corner to corner of the game canvas.
from: Vector2.zero(),
to: game.size,
child: Particle(),
)
)
),
),
);
```
## AcceleratedParticle
A basic physics particle which allows you to specify its initial `position`, `speed` and
`acceleration` and let the `update` cycle do the rest. All three are specified as `Vector2`s.
It works especially well for physics-based "bursts", but it is not limited to that.
The unit of the `Vector2` values for `speed` and `acceleration` are _logical px/s_. So a speed of
`Vector2(0, 100)` will move a child `Particle` by 100 logical pixels of the device every second of
game time.
`acceleration` and lets the `update` cycle do the rest. All three are specified as `Vector2`s, which
you can think of as vectors. It works especially well for physics-based "bursts", but it is not
limited to that. Unit of the `Vector2` value is _logical px/s_. So a speed of `Vector2(0, 100)` will
move a child `Particle` by 100 logical pixels of the device every second of game time.
```dart
final rng = Random();
@ -220,14 +217,14 @@ game.add(
ParticleComponent(
particle: AcceleratedParticle(
// Will fire off in the center of game canvas
position: game.size / 2,
// With a random initial speed of Vector2(-100..100, 0..-100)
speed: randomVector2(),
// Accelerates downwards, simulating "gravity"
position: game.size.center(Vector2.zero()),
// With random initial speed of Vector2(-100..100, 0..-100)
speed: Vector2(rnd.nextDouble() * 200 - 100, -rnd.nextDouble() * 100),
// Accelerating downwards, simulating "gravity"
speed: Vector2(0, 100),
child: Particle(),
)
)
),
),
);
```
@ -243,53 +240,56 @@ game.add(
particle: CircleParticle(
radius: game.size.x / 2,
paint: Paint()..color = Colors.red.withOpacity(.5),
)
)
),
),
);
```
## SpriteParticle
Allows you to embed a `Sprite` into your particle effect. It's useful when consuming graphics for
the effect from a `SpriteSheet` for example.
Allows you to embed a `Sprite` into your particle effects.
```dart
game.add(
ParticleComponent(
particle: SpriteParticle(
sprite: Sprite(spriteImage),
particle: SpriteParticle(
sprite: Sprite('sprite.png'),
size: Vector2(64, 64),
)
)
),
),
);
```
## ImageParticle
Renders the given `dart:ui` image within the particle tree.
Renders given `dart:ui` image within the particle tree.
```dart
// Within your game or component's `onLoad` method
await Flame.images.load('image.png');
// During game initialisation
await Flame.images.loadAll(const [
'image.png',
]);
// ...
// At some point during the game loop
// Somewhere during the game loop
final image = await Flame.images.load('image.png');
game.add(
ParticleComponent(
particle: ImageParticle(
size: Vector2.all(24),
image: Flame.images.fromCache('image.png'),
image: image,
);
)
),
);
```
## AnimationParticle
A `Particle` which embeds a Flame `Animation`. By default, it aligns the `Animation`s `stepTime` so
that it's fully played during the `Particle` lifespan. It's possible to override this behavior with
the `alignAnimationTime` parameter.
A `Particle` which embeds an `Animation`. By default, aligns the `Animation`'s `stepTime` so that
it's fully played during the `Particle` lifespan. It's possible to override this behavior with the
`alignAnimationTime` argument.
```dart
final spritesheet = SpriteSheet(
@ -302,40 +302,36 @@ game.add(
particle: AnimationParticle(
animation: spritesheet.createAnimation(0, stepTime: 0.1),
);
)
),
);
```
## ComponentParticle
This `Particle` allows you to embed a Flame `Component` within the particle effect. The `Component`
could have its own `update` lifecycle and could be reused across different particle effect trees.
If the only thing you need is to add some dynamics to an instance of a certain `Component`, please
consider adding it to the `game` directly and use the normal effects system for examples, without
the `Particle` in the middle.
This `Particle` allows you to embed a `Component` within the particle effects. The `Component` could
have its 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 a certain `Component`, please consider adding it
to the `game` directly, without the `Particle` in the middle.
```dart
var longLivingRect = RectComponent();
final longLivingRect = RectComponent();
game.add(
ParticleComponent(
particle: ComponentParticle(
component: longLivingRect
);
)
),
);
class RectComponent extends Component {
@override
void render(Canvas c) {
c.drawRect(
Rect.fromCenter(center: Offset.zero, width: 100, height: 100),
Rect.fromCenter(center: Offset.zero, width: 100, height: 100),
Paint()..color = Colors.red
);
}
@override
void update(double dt) {
/// Will be called by parent [Particle]
}
@ -347,8 +343,8 @@ class RectComponent extends Component {
To use Flare within Flame, use the [`flame_flare`](https://github.com/flame-engine/flame_flare)
package.
It will provide a class, `FlareParticle`, that is a container for `FlareActorAnimation` and it
propagates `update` and `render` hooks to its child.
It will provide a class called `FlareParticle` that is a container for `FlareActorAnimation`, it
propagates the `update` and `render` methods to its child.
```dart
import 'package:flame_flare/flame_flare.dart';
@ -362,8 +358,8 @@ flareAnimation.height = flareSize;
// Somewhere in game
game.add(
ParticleComponent(
particle: FlareParticle(flare: flareAnimation);
)
particle: FlareParticle(flare: flareAnimation),
),
);
```
@ -380,8 +376,7 @@ on each frame to perform necessary computations and render something to the `Can
```dart
game.add(
ParticleComponent(
// Renders a circle which gradually
// changes its color and size during the particle lifespan
// Renders a circle which gradually changes its color and size during the particle lifespan.
particle: ComputedParticle(
renderer: (canvas, particle) => canvas.drawCircle(
Offset.zero,
@ -393,8 +388,8 @@ game.add(
particle.progress,
),
),
)
)
),
),
)
```
@ -407,8 +402,12 @@ nesting these behaviors together to achieve the desired visual effect.
Two entities that allow `Particle`s to nest each other are: `SingleChildParticle` mixin and
`ComposedParticle` class.
A `SingleChildParticle` may help you with creating `Particles` with a custom behavior.
For example, randomly positioning its child during each frame:
A `SingleChildParticle` may help you with creating `Particles` with a custom behavior. For example,
randomly positioning its child during each frame:
The `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();

View File

@ -84,7 +84,7 @@ class ParticlesGame extends BaseGame {
// lifecycle from the [BaseGame].
TranslatedParticle(
lifespan: 1,
offset: cellCenter.toOffset(),
offset: cellCenter,
child: particle,
).asComponent(),
);
@ -112,10 +112,9 @@ class ParticlesGame extends BaseGame {
/// one predefined position to another one
Particle movingParticle() {
return MovingParticle(
// This parameter is optional, will
// default to [Offset.zero]
from: const Offset(-20, -20),
to: const Offset(20, 20),
/// This parameter is optional, will default to [Vector2.zero]
from: Vector2(-20, -20),
to: Vector2(20, 20),
child: CircleParticle(paint: Paint()..color = Colors.amber),
);
}
@ -124,7 +123,7 @@ class ParticlesGame extends BaseGame {
/// within each cell each time created
Particle randomMovingParticle() {
return MovingParticle(
to: randomCellOffset(),
to: randomCellVector2(),
child: CircleParticle(
radius: 5 + rnd.nextDouble() * 5,
paint: Paint()..color = Colors.red,
@ -140,8 +139,8 @@ class ParticlesGame extends BaseGame {
generator: (i) {
final currentColumn = (cellSize.x / 5) * i - halfCellSize.x;
return MovingParticle(
from: Offset(currentColumn, -halfCellSize.y),
to: Offset(currentColumn, halfCellSize.y),
from: Vector2(currentColumn, -halfCellSize.y),
to: Vector2(currentColumn, halfCellSize.y),
child: CircleParticle(
radius: 2.0,
paint: Paint()..color = Colors.blue,
@ -157,7 +156,7 @@ class ParticlesGame extends BaseGame {
return Particle.generate(
count: 5,
generator: (i) => MovingParticle(
to: randomCellOffset() * .5,
to: randomCellVector2()..scale(.5),
child: CircleParticle(
radius: 5 + rnd.nextDouble() * 5,
paint: Paint()..color = Colors.deepOrange,
@ -173,7 +172,7 @@ class ParticlesGame extends BaseGame {
count: 5,
generator: (i) => MovingParticle(
curve: Curves.easeOutQuad,
to: randomCellOffset() * .5,
to: randomCellVector2()..scale(.5),
child: CircleParticle(
radius: 5 + rnd.nextDouble() * 5,
paint: Paint()..color = Colors.deepPurple,
@ -192,7 +191,7 @@ class ParticlesGame extends BaseGame {
count: 5,
generator: (i) => MovingParticle(
curve: const Interval(.2, .6, curve: Curves.easeInOutCubic),
to: randomCellOffset() * .5,
to: randomCellVector2()..scale(.5),
child: CircleParticle(
radius: 5 + rnd.nextDouble() * 5,
paint: Paint()..color = Colors.greenAccent,
@ -258,7 +257,7 @@ class ParticlesGame extends BaseGame {
return Particle.generate(
generator: (i) => MovingParticle(
curve: Interval(rnd.nextDouble() * .1, rnd.nextDouble() * .8 + .1),
to: randomCellOffset() * .5,
to: randomCellVector2()..scale(.5),
child: reusablePatricle!,
),
);
@ -292,7 +291,7 @@ class ParticlesGame extends BaseGame {
return Particle.generate(
count: count,
generator: (i) => TranslatedParticle(
offset: Offset(
offset: Vector2(
(i % perLine) * colWidth - halfCellSize.x + imageSize,
(i ~/ perLine) * rowHeight - halfCellSize.y + imageSize,
),
@ -316,8 +315,8 @@ class ParticlesGame extends BaseGame {
return Particle.generate(
generator: (i) => AcceleratedParticle(
speed:
Offset(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .2,
acceleration: const Offset(0, 200),
Vector2(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .2,
acceleration: Vector2(0, 200),
child: rotatingImage(initialAngle: rnd.nextDouble() * pi),
),
);
@ -334,9 +333,9 @@ class ParticlesGame extends BaseGame {
const Color(0xff0000ff),
];
final positions = [
const Offset(-10, 10),
const Offset(10, 10),
const Offset(0, -14),
Vector2(-10, 10),
Vector2(10, 10),
Vector2(0, -14),
];
return Particle.generate(
@ -380,8 +379,8 @@ class ParticlesGame extends BaseGame {
/// which is independent from the parent [Particle].
Particle componentParticle() {
return MovingParticle(
from: (-halfCellSize * .2).toOffset(),
to: (halfCellSize * .2).toOffset(),
from: -halfCellSize * .2,
to: halfCellSize * .2,
curve: SineCurve(),
child: ComponentParticle(component: trafficLight),
);
@ -406,9 +405,9 @@ class ParticlesGame extends BaseGame {
return Particle.generate(
generator: (i) {
final initialSpeed = randomCellOffset();
final initialSpeed = randomCellVector2();
final deceleration = initialSpeed * -1;
const gravity = Offset(0, 40);
final gravity = Vector2(0, 40);
return AcceleratedParticle(
speed: initialSpeed,
@ -447,23 +446,20 @@ class ParticlesGame extends BaseGame {
),
);
final cellSizeOffset = cellSize.toOffset();
final halfCellSizeOffset = halfCellSize.toOffset();
return ComposedParticle(
children: [
rect
.rotating(to: pi / 2)
.moving(to: -cellSizeOffset)
.moving(to: -cellSize)
.scaled(2)
.accelerated(acceleration: halfCellSizeOffset * 5)
.translated(halfCellSizeOffset),
.accelerated(acceleration: halfCellSize * 5)
.translated(halfCellSize),
rect
.rotating(to: -pi)
.moving(to: cellSizeOffset.scale(1, -1))
.moving(to: Vector2(1, -1)..multiply(cellSize))
.scaled(2)
.translated(halfCellSizeOffset.scale(-1, 1))
.accelerated(acceleration: halfCellSizeOffset.scale(-5, 5)),
.translated(Vector2(1, -1)..multiply(halfCellSize))
.accelerated(acceleration: Vector2(-5, 5)..multiply(halfCellSize)),
],
);
}
@ -484,13 +480,9 @@ class ParticlesGame extends BaseGame {
}
}
/// Returns random [Offset] within a virtual
/// grid cell
Offset randomCellOffset() {
return Offset(
cellSize.x * rnd.nextDouble() - halfCellSize.x,
cellSize.y * rnd.nextDouble() - halfCellSize.y,
);
/// Returns random [Vector2] within a virtual grid cell
Vector2 randomCellVector2() {
return (Vector2.random() - Vector2.random())..multiply(cellSize);
}
/// Returns random [Color] from primary swatches

View File

@ -7,6 +7,7 @@
- Fix anchor of rendered text in TextComponent
- Add new extensions to handle math.Rectangles nicely
- Implement color parsing methods
- Migrated the `Particle` API to `Vector2`
## [1.0.0-releasecandidate.11]
- Replace deprecated analysis option lines-of-executable-code with source-lines-of-code

View File

@ -16,7 +16,7 @@ class ParticleComponent extends Component {
/// This [ParticleComponent] will be removed by the BaseGame.
@override
bool get shouldRemove => particle.shouldRemove();
bool get shouldRemove => particle.shouldRemove;
/// Returns progress of the child [Particle].
///

View File

@ -98,4 +98,10 @@ extension Vector2Extension on Vector2 {
/// Create a Vector2 with ints as input
static Vector2 fromInts(int x, int y) => Vector2(x.toDouble(), y.toDouble());
/// Creates a heading [Vector2] with the given angle in radians.
static Vector2 fromRadians(double r) => Vector2.zero()..rotate(r);
/// Creates a heading [Vector2] with the given angle in degrees.
static Vector2 fromDegrees(double d) => fromRadians(d * degrees2Radians);
}

View File

@ -1,36 +1,43 @@
import 'dart:ui';
import '../../extensions.dart';
import '../components/mixins/single_child_particle.dart';
import 'curved_particle.dart';
import 'particle.dart';
/// A particle serves as a container for basic
/// acceleration physics.
/// [Offset] unit is logical px per second.
/// speed = Offset(0, 100) is 100 logical pixels per second, down
/// acceleration = Offset(-40, 0) will accelerate to left at rate of 40 px/s
/// A particle that serves as a container for basic acceleration physics.
///
/// [speed] is logical px per second.
///
/// ```dart
/// AcceleratedParticle(
/// speed: Vector2(0, 100), // is 100 logical px/s down.
/// acceleration: Vector2(-40, 0) // will accelerate to the left at rate of 40 px/s
/// )
/// ```
class AcceleratedParticle extends CurvedParticle with SingleChildParticle {
@override
Particle child;
final Offset acceleration;
Offset speed;
Offset position;
final Vector2 acceleration;
Vector2 speed;
Vector2 position;
AcceleratedParticle({
required this.child,
this.acceleration = Offset.zero,
this.speed = Offset.zero,
this.position = Offset.zero,
Vector2? acceleration,
Vector2? speed,
Vector2? position,
double? lifespan,
}) : super(
lifespan: lifespan,
);
}) : acceleration = acceleration ?? Vector2.zero(),
position = position ?? Vector2.zero(),
speed = speed ?? Vector2.zero(),
super(lifespan: lifespan);
@override
void render(Canvas canvas) {
canvas.save();
canvas.translate(position.dx, position.dy);
canvas.translateVector(position);
super.render(canvas);
canvas.restore();
}
@ -39,7 +46,6 @@ class AcceleratedParticle extends CurvedParticle with SingleChildParticle {
void update(double dt) {
speed += acceleration * dt;
position += speed * dt - (acceleration * dt * dt) / 2;
super.update(dt);
}
}

View File

@ -2,9 +2,9 @@ import 'dart:ui';
import 'particle.dart';
/// Plain circle with no other behaviors
/// Consider composing with other [Particle]
/// to achieve needed effects
/// Plain circle with no other behaviors.
///
/// Consider composing this with other [Particle]s to achieve needed effects.
class CircleParticle extends Particle {
final Paint paint;
final double radius;
@ -13,9 +13,7 @@ class CircleParticle extends Particle {
required this.paint,
this.radius = 10.0,
double? lifespan,
}) : super(
lifespan: lifespan,
);
}) : super(lifespan: lifespan);
@override
void render(Canvas c) {

View File

@ -2,35 +2,35 @@ import 'dart:ui';
import 'package:flutter/animation.dart';
import '../../extensions.dart';
import '../components/mixins/single_child_particle.dart';
import '../particles/curved_particle.dart';
import 'particle.dart';
/// Statically offset given child [Particle] by given [Offset]
/// If you're looking to move the child, consider [MovingParticle]
/// Statically move given child [Particle] by given [Vector2].
///
/// If you're looking to move the child, consider the [MovingParticle].
class MovingParticle extends CurvedParticle with SingleChildParticle {
@override
Particle child;
final Offset from;
final Offset to;
final Vector2 from;
final Vector2 to;
MovingParticle({
required this.child,
required this.to,
this.from = Offset.zero,
Vector2? from,
double? lifespan,
Curve curve = Curves.linear,
}) : super(
lifespan: lifespan,
curve: curve,
);
}) : from = from ?? Vector2.zero(),
super(lifespan: lifespan, curve: curve);
@override
void render(Canvas c) {
c.save();
final current = Offset.lerp(from, to, progress)!;
c.translate(current.dx, current.dy);
final current = from.clone()..lerp(to, progress);
c.translateVector(current);
super.render(c);
c.restore();
}

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/animation.dart';
import '../../extensions.dart';
import '../components/component.dart';
import '../components/particle_component.dart';
import '../timer.dart';
@ -13,18 +14,19 @@ import 'rotating_particle.dart';
import 'scaled_particle.dart';
import 'translated_particle.dart';
/// A function which returns [Particle] when called
/// A function which returns a [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.
/// Intention is to follow the same "Extreme Composability" style as seen across
/// the whole Flutter framework. Each type of particle implements some
/// particular behavior which then could be nested and combined to create
/// the experience you are looking for.
abstract class Particle {
/// Generates given amount of particles,
/// combining them into one [ComposedParticle]
/// Generates a given amount of particles and then combining them into one
/// single [ComposedParticle].
///
/// Useful for procedural particle generation.
static Particle generate({
int count = 10,
@ -37,71 +39,69 @@ abstract class Particle {
);
}
/// Internal timer defining how long
/// this [Particle] will live. [Particle] will
/// be marked for removal when this timer is over.
/// Internal timer defining how long this [Particle] will live.
///
/// [Particle] will be marked for removal when this timer is over.
Timer? _timer;
/// Stores desired lifespan of the
/// particle in seconds.
/// Stores desired lifespan of the particle in seconds.
late double _lifespan;
/// Will be set to true by update hook
/// when this [Particle] reaches end of its lifespan
/// Will be set to true by [update] when this [Particle] reaches the end of
/// its lifespan.
bool _shouldBeRemoved = false;
/// Construct a new [Particle].
///
/// The [lifespan] is how long this [Particle] will live in seconds, with
/// microsceond precision.
Particle({
/// Particle lifespan in [Timer] format,
/// double in seconds with microsecond precision
double? lifespan,
}) {
setLifespan(lifespan ?? .5);
}
/// This method will return true as
/// soon as particle reaches an end of its
/// lifespan, which means it's ready to be
/// removed by a wrapping container.
/// Follows same style as [Component].
bool shouldRemove() => _shouldBeRemoved;
/// This method will return true as soon as the particle reaches the end of
/// its lifespan.
///
/// It will then be ready to be removed by a wrapping container.
bool get shouldRemove => _shouldBeRemoved;
/// Getter which should be used by subclasses
/// to get overall progress. Also allows to substitute
/// progress with other values, for example adding easing as in CurvedParticle.
/// Getter which should be used by subclasses to get overall progress.
///
/// Also allows to substitute progress with other values, for example adding
/// easing as in CurvedParticle.
double get progress => _timer?.progress ?? 0.0;
/// 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
}
///
/// Default behavior is empty, so that it's not required to override this in
/// a [Particle] that renders nothing and serve as a behavior container.
void render(Canvas canvas) {}
/// Updates internal [Timer] of this [Particle]
/// which defines its position on the lifespan.
/// Marks [Particle] for removal when it is over.
/// Updates the [_timer] of this [Particle].
void update(double dt) {
_timer?.update(dt);
}
/// 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.
/// 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) {
// TODO: Maybe make it into a setter/getter?
_lifespan = lifespan;
_timer?.stop();
void removeCallback() => _shouldBeRemoved = true;
_timer = Timer(lifespan, callback: removeCallback);
_timer!.start();
_timer = Timer(lifespan, callback: () => _shouldBeRemoved = true)..start();
}
/// Wraps this particle with [TranslatedParticle]
/// statically repositioning it for the time
/// of the lifespan.
Particle translated(Offset offset) {
/// Wraps this particle with a [TranslatedParticle].
///
/// Statically repositioning it for the time of the lifespan.
Particle translated(Vector2 offset) {
return TranslatedParticle(
offset: offset,
child: this,
@ -109,16 +109,16 @@ abstract class Particle {
);
}
/// Wraps this particle with [MovingParticle]
/// allowing it to move from one [Offset]
/// on the canvas to another one.
/// Wraps this particle with a [MovingParticle].
///
/// Allowing it to move from one [Vector2] to another one.
Particle moving({
Offset from = Offset.zero,
required Offset to,
Vector2? from,
required Vector2 to,
Curve curve = Curves.linear,
}) {
return MovingParticle(
from: from,
from: from ?? Vector2.zero(),
to: to,
curve: curve,
child: this,
@ -126,26 +126,26 @@ abstract class Particle {
);
}
/// Wraps this particle with [AcceleratedParticle]
/// allowing to specify desired position speed and acceleration
/// and leave the basic physics do the rest.
/// Wraps this particle with a [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,
required Vector2 acceleration,
Vector2? position,
Vector2? speed,
}) {
return AcceleratedParticle(
position: position,
speed: speed,
position: position ?? Vector2.zero(),
speed: speed ?? Vector2.zero(),
acceleration: acceleration,
child: this,
lifespan: _lifespan,
);
}
/// Rotates this particle to a fixed angle
/// in radians with [RotatingParticle]
Particle rotated([double angle = 0]) {
/// Rotates this particle to a fixed angle in radians using [RotatingParticle].
Particle rotated(double angle) {
return RotatingParticle(
child: this,
lifespan: _lifespan,
@ -154,8 +154,8 @@ abstract class Particle {
);
}
/// Rotates this particle from given angle
/// to another one in radians with [RotatingParticle]
/// Rotates this particle from a given angle to another one in radians
/// using [RotatingParticle].
Particle rotating({
double from = 0,
double to = pi,
@ -168,15 +168,15 @@ abstract class Particle {
);
}
/// Wraps this particle with [ScaledParticle]
/// allowing to change size of it and/or its children
/// Wraps this particle with a [ScaledParticle].
///
/// Allows for chainging the size of this particle and/or its children.
Particle scaled(double scale) {
return ScaledParticle(scale: scale, child: this, lifespan: _lifespan);
}
/// Wraps this particle with [ParticleComponent]
/// to be used within the BaseGame component system.
Component asComponent() {
return ParticleComponent(particle: this);
}
/// Wraps this particle with a [ParticleComponent].
///
/// Should be used with the FCS.
Component asComponent() => ParticleComponent(particle: this);
}

View File

@ -1,27 +1,28 @@
import 'dart:ui';
import '../../extensions.dart';
import '../components/mixins/single_child_particle.dart';
import 'particle.dart';
/// Statically offset given child [Particle] by given [Offset]
/// Statically offset given child [Particle] by given [Vector2].
///
/// If you're looking to move the child, consider MovingParticle.
class TranslatedParticle extends Particle with SingleChildParticle {
@override
Particle child;
Offset offset;
final Vector2 offset;
TranslatedParticle({
required this.child,
required this.offset,
double? lifespan,
}) : super(
lifespan: lifespan,
);
}) : super(lifespan: lifespan);
@override
void render(Canvas c) {
c.save();
c.translate(offset.dx, offset.dy);
c.translateVector(offset);
super.render(c);
c.restore();
}