feat: particles

This commit is contained in:
Ivan Cherepanov
2019-11-25 17:58:16 +03:00
parent bec146afa5
commit 38e3685cda
20 changed files with 982 additions and 0 deletions

View 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);
}
}

View 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();
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}

View 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());
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}