Effect Samples
In the upcoming section, we will explore how to create animations using Newtonian dynamics, such as simulating rain or pulse effects. These animations can be constructed either deterministically or by applying relativistic principles.
Rain
To create a rain effect, particles are randomly emitted along the x-axis, with their y offset initialized to zero (the origin is relative to the size of the Newton widget). We set the origin to Offset.zero
, and define the minimum and maximum origin offsets as Offset.zero
and Offset(1, 0)
, respectively. The minimum and maximum distances are set to the height of the screen (since our Newton widget spans the entire screen). The particle angle is set to 90° to make them fall downward. We introduce some randomness in fade-out times to ensure that particles fade at different rates, and we also randomize the lifespan of each particle to enhance the natural look of the rain.
DeterministicEffectConfiguration(
minDistance: screenSize.height,
maxDistance: screenSize.height,
origin: Offset.zero,
maxOriginOffset: const Offset(1, 0),
maxAngle: 90,
maxParticleLifespan: const Duration(seconds: 7),
maxEndScale: 1,
maxFadeOutThreshold: .8,
minAngle: 90,
minParticleLifespan: const Duration(seconds: 4),
minEndScale: 1,
minFadeOutThreshold: .6,
particleConfiguration: const ParticleConfiguration(
shape: CircleShape(),
size: Size(5, 5),
),
)
Fountain
The deterministic fountain animation requires constructing a custom path using a quadratic Bézier curve. The process starts by defining a random horizontal displacement (randomWidth
) and a vertical distance (distance
).
moveTo()
sets the initial position of the particle.relativeQuadraticBezierTo()
creates the curved path that simulates the fountain's trajectory. The first two parameters define the control point, giving the fountain its upward arc, while the last two define the end point, determining how far and wide the particle will travel.
The randomness in width and distance adds variability, making the fountain look more natural by altering the particles' paths dynamically.
DeterministicEffectConfiguration(
minParticleLifespan: const Duration(seconds: 4),
maxParticleLifespan: const Duration(seconds: 4),
customPathBuilder: (effect, animatedParticle) {
const width = 60;
final path = Path();
final randomWidth = random.nextDoubleRange(-width / 2, width / 2);
final distance = effect.effectConfiguration.randomDistance();
// Define the Bezier path to simulate the fountain trajectory
return PathMetricsTransformation(
path: path
..moveTo(animatedParticle.particle.initialPosition.dx, animatedParticle.particle.initialPosition.dy)
..relativeQuadraticBezierTo(
randomWidth,
-distance,
randomWidth * 4,
-distance / Random().nextIntRange(2, 6),
),
);
},
minDistance: 200,
minFadeOutThreshold: .6,
maxFadeOutThreshold: .8,
minEndScale: 1,
maxEndScale: 1,
particleConfiguration: const ParticleConfiguration(
shape: CircleShape(),
size: Size(5, 5),
),
particlesPerEmit: 10,
)
Pulse
For the pulse animation, we use a customPathBuilder
to compute the emission angle for each particle. The angle is calculated based on the total number of particles emitted per pulse and the count of particles already emitted. This ensures that the particles are evenly distributed in a circular pattern. All particles use the same animation duration, so they travel at the same pace, creating a synchronized and cohesive pulse effect.
DeterministicEffectConfiguration(
customPathBuilder: (effect, animatedParticle) {
final particlesPerEmit = effect.effectConfiguration.particlesPerEmit;
final angle = 360 / particlesPerEmit * (effect.activeParticles.length % particlesPerEmit);
return StraightPathTransformation(distance: effect.effectConfiguration.randomDistance(), angle: angle);
},
emitDuration: const Duration(seconds: 1),
maxParticleLifespan: const Duration(seconds: 4),
maxEndScale: 1,
maxFadeOutThreshold: .8,
minDistance: 200,
minEndScale: 1,
minParticleLifespan: const Duration(seconds: 4),
minFadeOutThreshold: .8,
particleConfiguration: const ParticleConfiguration(
shape: CircleShape(),
size: Size(5, 5),
),
particlesPerEmit: 15,
)
Smoke
For the smoke effect, particles are emitted upward with a small angle randomness to simulate natural dispersion. We adjust the minOriginOffset
and maxOriginOffset
so that particles are emitted from slightly different locations, adding variability to the source. Additionally, we introduce randomness in the animation duration, which causes some particles to move slower than others, enhancing the realistic appearance of the smoke.
DeterministicEffectConfiguration(
minAngle: -100,
maxAngle: -80,
minOriginOffset: const Offset(-.01, 0),
minParticleLifespan: const Duration(seconds: 4),
maxParticleLifespan: const Duration(seconds: 7),
minFadeOutThreshold: .6,
maxFadeOutThreshold: .8,
maxOriginOffset: const Offset(.01, 0),
minEndScale: 1,
maxEndScale: 1,
particleConfiguration: const ParticleConfiguration(
shape: CircleShape(),
size: Size(5, 5),
),
particlesPerEmit: 3,
)
Firework
The firework effect introduces a key concept: the postEffectBuilder
in the particle configuration. This allows triggering a new effect when a particle's lifespan ends. For the firework, we emit particles upward with some randomness in their angle. In the postEffectBuilder
, we create an explosion effect at the last particle's position, using that as the origin for the explosion. Additionally, we enable the trail effect to simulate the fire trail of the launching firework, making the animation more realistic.
Note: You can mix both relativistic and deterministic effects, though particles from each effect will not interact with each other.
DeterministicEffectConfiguration(
minAngle: -120,
maxAngle: -60,
maxParticleLifespan: const Duration(seconds: 2),
minFadeOutThreshold: .6,
maxFadeOutThreshold: .8,
emitDuration: const Duration(milliseconds: 500),
minEndScale: 1,
maxEndScale: 1,
particleConfiguration: ParticleConfiguration(
shape: const CircleShape(),
size: const Size(5, 5),
postEffectBuilder: (particle, effect) {
final offset = Offset(
particle.position.dx / effect.surfaceSize.width,
particle.position.dy / effect.surfaceSize.height,
);
return DeterministicEffectConfiguration(
maxAngle: 180,
minAngle: -180,
particleCount: 10,
particleConfiguration: const ParticleConfiguration(
shape: CircleShape(),
size: Size(5, 5),
color: SingleParticleColor(color: Colors.blue),
),
particlesPerEmit: 10,
distanceCurve: Curves.decelerate,
origin: offset,
);
},
),
trail: const StraightTrail(
trailWidth: 3,
trailProgress: .3,
),
)