flutter3d
Showcase Changelog 38 packages API reference

Over-life curves

since 0.5.0 Physics and particles

ParticleSizeOverLife only ever goes from one number to another, and ParticleFade only ever goes down. A puff of smoke that grows and then shrinks needs both shapes on the same value, which is what a curve is for: a value described as a handful of keys, sampled wherever a particle's life happens to be.

Step 1: A curve for size #

Each CurveKey sits at a point in [0, 1] of the particle's life. Between two keys the value is eased by whichever shape the earlier key names, and outside the first and last key the curve holds still rather than extrapolating.

// Puffs up to one and a half times its birth size by the middle of its
// life, then shrinks away, easing smoothly through the peak rather than
// arriving at it with a corner.
_size = ParticleCurve(<CurveKey>[
  CurveKey(0.0, 0.4, ease: KeyEase.smooth),
  CurveKey(0.5, 1.5, ease: KeyEase.smooth),
  CurveKey(1.0, 0.0),
]);

Step 2: A gradient for colour #

A ParticleGradient is the same idea for a colour: GradientKeys instead of numbers, sampled the same way. Four independent channel curves would let the keys drift apart and produce a grey frame nobody asked for; a gradient's keys are keys of the whole colour at once.

_color = ParticleGradient(<GradientKey>[
  GradientKey(0.0, Vector4(1.0, 0.95, 0.6, 1.0)),
  GradientKey(0.4, Vector4(1.0, 0.4, 0.1, 1.0), ease: KeyEase.smooth),
  GradientKey(1.0, Vector4(0.2, 0.05, 0.05, 0.0)),
]);

Step 3: Read by an affector #

A curve and a gradient are values, not affectors. ParticleSizeCurve and ParticleColorGradient are the small pieces that read them every step and write the result onto the particle, the same shape every other affector on the modifiers page takes.

_smokeEffect = ParticleEffect(
  count: _count,
  emitter: const DriftEmitter(speed: Range(0.4, 0.9)),
  lifetime: const Range(1.2, 2.0),
  size: const Range(0.08, 0.12),
  color: Vector4(1.0, 1.0, 1.0, 1.0),
  affectors: <ParticleAffector>[
    ParticleSizeCurve(_size),
    ParticleColorGradient(_color),
  ],
);

Step 4: What the shape actually is #

Neither a curve nor a gradient needs a running particle to check: they are plain data, sampled directly. This page's own check does exactly that, off the same two objects the burst above draws with.

// The curve holds its end values outside its keys and rises then falls
// in between, which is what "puffs up, then shrinks" means as numbers.
if (_size.sample(0.0) >= _size.sample(0.5) ||
    _size.sample(0.5) <= _size.sample(1.0)) {
  throw StateError('the size curve should rise then fall');
}
// The gradient fades to nothing: alpha at the last key is zero.
final Vector4 end = Vector4.zero();
_color.sampleInto(end, 1.0);
if (end.w != 0.0) {
  throw StateError('the gradient should fade to nothing');
}

Note. KeyEase.smooth eases into and out of a key rather than turning a straight corner there, which is what keeps a size that puffs up and back down from looking like it hit a wall in the middle.