mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 11:43:19 +08:00
feat: ⚡particles ⚡
This commit is contained in:
44
lib/components/mixins/single_child_particle.dart
Normal file
44
lib/components/mixins/single_child_particle.dart
Normal file
@ -0,0 +1,44 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import '../particle_component.dart';
|
||||
|
||||
/// Implements basic behavior for nesting [Particle] instances
|
||||
/// into each other.
|
||||
///
|
||||
/// ```dart
|
||||
/// class BehaviorParticle extends Particle with SingleChildParticle {
|
||||
/// Particle child;
|
||||
///
|
||||
/// BehaviorParticle({
|
||||
/// @required this.child
|
||||
/// });
|
||||
///
|
||||
/// @override
|
||||
/// update(double dt) {
|
||||
/// // Will ensure that child [Particle] is properly updated
|
||||
/// super.update(dt);
|
||||
///
|
||||
/// // ... Custom behavior
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
mixin SingleChildParticle on Particle {
|
||||
Particle child;
|
||||
|
||||
@override
|
||||
void setLifespan(double lifespan) {
|
||||
super.setLifespan(lifespan);
|
||||
child.setLifespan(lifespan);
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas c) {
|
||||
child.render(c);
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double t) {
|
||||
super.update(t);
|
||||
child.update(t);
|
||||
}
|
||||
}
|
||||
80
lib/components/particle_component.dart
Normal file
80
lib/components/particle_component.dart
Normal file
@ -0,0 +1,80 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components/particles/composed_particle.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../time.dart';
|
||||
import 'component.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 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<Particle>.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);
|
||||
}
|
||||
|
||||
@override
|
||||
bool destroy() => _shouldBeDestroyed;
|
||||
double get progress => _timer.progress;
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
46
lib/components/particles/accelerated_particle.dart
Normal file
46
lib/components/particles/accelerated_particle.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../mixins/single_child_particle.dart';
|
||||
import '../particle_component.dart';
|
||||
import 'curved_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
|
||||
class AcceleratedParticle extends CurvedParticle with SingleChildParticle {
|
||||
@override
|
||||
Particle child;
|
||||
|
||||
final Offset acceleration;
|
||||
Offset speed;
|
||||
Offset position;
|
||||
|
||||
AcceleratedParticle({
|
||||
@required this.child,
|
||||
this.acceleration = Offset.zero,
|
||||
this.speed = Offset.zero,
|
||||
this.position = Offset.zero,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(lifespan: lifespan, duration: duration);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.translate(position.dx, position.dy);
|
||||
super.render(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double t) {
|
||||
speed += acceleration * t;
|
||||
position += speed * t;
|
||||
|
||||
super.update(t);
|
||||
}
|
||||
}
|
||||
29
lib/components/particles/circle_particle.dart
Normal file
29
lib/components/particles/circle_particle.dart
Normal file
@ -0,0 +1,29 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../time.dart';
|
||||
import '../particle_component.dart';
|
||||
|
||||
/// Plain circle with no other behaviors
|
||||
/// Consider composing with other [Particle]
|
||||
/// to achieve needed effects
|
||||
class CircleParticle extends Particle {
|
||||
final Paint paint;
|
||||
final double radius;
|
||||
|
||||
CircleParticle({
|
||||
@required this.paint,
|
||||
this.radius = 10.0,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas c) {
|
||||
c.drawCircle(Offset.zero, radius, paint);
|
||||
}
|
||||
}
|
||||
43
lib/components/particles/composed_particle.dart
Normal file
43
lib/components/particles/composed_particle.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../particle_component.dart';
|
||||
|
||||
class ComposedParticle extends Particle {
|
||||
final List<Particle> children;
|
||||
|
||||
ComposedParticle({
|
||||
@required this.children,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void setLifespan(double lifespan) {
|
||||
super.setLifespan(lifespan);
|
||||
|
||||
for (var child in children) {
|
||||
child.setLifespan(lifespan);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas c) {
|
||||
for (var child in children) {
|
||||
child.render(c);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void update(double dt) {
|
||||
super.update(dt);
|
||||
|
||||
for (var child in children) {
|
||||
child.update(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/components/particles/computed_particle.dart
Normal file
32
lib/components/particles/computed_particle.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../particle_component.dart';
|
||||
|
||||
/// A function which should render desired contents
|
||||
/// onto a given canvas. External state needed for
|
||||
/// rendering should be stored elsewhere, so that this delegate could use it
|
||||
typedef ParticleRenderDelegate = void Function(Canvas c, Particle particle);
|
||||
|
||||
/// An abstract [Particle] container which delegates renderign outside
|
||||
/// Allows to implement very interesting scenarios from scratch.
|
||||
class ComputedParticle extends Particle {
|
||||
// A delegate function which will be called
|
||||
// to render particle on each frame
|
||||
ParticleRenderDelegate renderer;
|
||||
|
||||
ComputedParticle({
|
||||
@required this.renderer,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
renderer(canvas, this);
|
||||
}
|
||||
}
|
||||
16
lib/components/particles/curved_particle.dart
Normal file
16
lib/components/particles/curved_particle.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'package:flutter/animation.dart';
|
||||
|
||||
import '../particle_component.dart';
|
||||
|
||||
class CurvedParticle extends Particle {
|
||||
final Curve curve;
|
||||
|
||||
CurvedParticle({
|
||||
this.curve = Curves.linear,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(lifespan: lifespan, duration: duration);
|
||||
|
||||
@override
|
||||
double get progress => curve.transform(super.progress);
|
||||
}
|
||||
33
lib/components/particles/image_particle.dart
Normal file
33
lib/components/particles/image_particle.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../particle_component.dart';
|
||||
|
||||
class ImageParticle extends Particle {
|
||||
/// dart.ui [Image] to draw
|
||||
Image image;
|
||||
|
||||
Rect src;
|
||||
Rect dest;
|
||||
|
||||
ImageParticle({
|
||||
@required this.image,
|
||||
Size size,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(lifespan: lifespan, duration: duration) {
|
||||
final srcWidth = image.width.toDouble();
|
||||
final srcHeight = image.height.toDouble();
|
||||
final destWidth = size?.width ?? srcWidth;
|
||||
final destHeight = size?.height ?? srcHeight;
|
||||
|
||||
src = Rect.fromLTWH(0, 0, srcWidth, srcHeight);
|
||||
dest = Rect.fromLTWH(-destWidth / 2, -destHeight / 2, destWidth, destHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.drawImageRect(image, src, dest, Paint());
|
||||
}
|
||||
}
|
||||
40
lib/components/particles/moving_particle.dart
Normal file
40
lib/components/particles/moving_particle.dart
Normal file
@ -0,0 +1,40 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/foundation.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]
|
||||
class MovingParticle extends CurvedParticle with SingleChildParticle {
|
||||
@override
|
||||
Particle child;
|
||||
|
||||
final Offset from;
|
||||
final Offset to;
|
||||
|
||||
MovingParticle({
|
||||
@required this.child,
|
||||
@required this.to,
|
||||
this.from = Offset.zero,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas c) {
|
||||
c.save();
|
||||
final Offset current = Offset.lerp(from, to, progress);
|
||||
c.translate(current.dx, current.dy);
|
||||
super.render(c);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
44
lib/components/particles/paint_particle.dart
Normal file
44
lib/components/particles/paint_particle.dart
Normal file
@ -0,0 +1,44 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../mixins/single_child_particle.dart';
|
||||
import '../particle_component.dart';
|
||||
import 'curved_particle.dart';
|
||||
|
||||
/// A particle which renders its child with certain [Paint]
|
||||
/// Could be used for applying composite effects.
|
||||
/// Be aware that any composite operation is relatively expensive, as involves
|
||||
/// copying portions of GPU memory. The less pixels copied - the faster it'll be.
|
||||
class PaintParticle extends CurvedParticle with SingleChildParticle {
|
||||
@override
|
||||
Particle child;
|
||||
|
||||
final Paint paint;
|
||||
|
||||
/// Defines Canvas layer bounds
|
||||
/// for applying this particle composite effect.
|
||||
/// Any child content outside this bounds will be clipped.
|
||||
final Rect bounds;
|
||||
|
||||
PaintParticle({
|
||||
@required this.child,
|
||||
@required this.paint,
|
||||
|
||||
// Reasonably large rect for most particles
|
||||
this.bounds = const Rect.fromLTRB(-50, -50, 50, 50),
|
||||
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.saveLayer(bounds, paint);
|
||||
super.render(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
36
lib/components/particles/rotating_particle.dart
Normal file
36
lib/components/particles/rotating_particle.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../mixins/single_child_particle.dart';
|
||||
import '../particle_component.dart';
|
||||
import 'curved_particle.dart';
|
||||
|
||||
/// A particle which rotates its child over the lifespan
|
||||
/// between two given bounds in radians
|
||||
class RotatingParticle extends CurvedParticle with SingleChildParticle {
|
||||
@override
|
||||
Particle child;
|
||||
|
||||
final double from;
|
||||
final double to;
|
||||
|
||||
RotatingParticle({
|
||||
@required this.child,
|
||||
this.from = 0,
|
||||
this.to = 2 * pi,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(lifespan: lifespan, duration: duration);
|
||||
|
||||
double get angle => lerpDouble(from, to, progress);
|
||||
|
||||
@override
|
||||
void render(Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.rotate(angle);
|
||||
super.render(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
33
lib/components/particles/translated_particle.dart
Normal file
33
lib/components/particles/translated_particle.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components/mixins/single_child_particle.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../time.dart';
|
||||
import '../particle_component.dart';
|
||||
|
||||
/// Statically offset given child [Particle] by given [Offset]
|
||||
/// If you're loking to move the child, consider [MovingParticle]
|
||||
class TranslatedParticle extends Particle with SingleChildParticle {
|
||||
@override
|
||||
Particle child;
|
||||
Offset offset;
|
||||
|
||||
TranslatedParticle({
|
||||
@required this.child,
|
||||
@required this.offset,
|
||||
double lifespan,
|
||||
Duration duration,
|
||||
}) : super(
|
||||
lifespan: lifespan,
|
||||
duration: duration,
|
||||
);
|
||||
|
||||
@override
|
||||
void render(Canvas c) {
|
||||
c.save();
|
||||
c.translate(offset.dx, offset.dy);
|
||||
super.render(c);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user