mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
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:
committed by
GitHub
parent
609ab27d69
commit
021b453b54
263
doc/particles.md
263
doc/particles.md
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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].
|
||||
///
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user