Skip to content

rajajain08/particles_flutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

131 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

particles_flutter

High-performance, fully customisable particle animations for Flutter. Starfields, snow, confetti, fireworks, connected webs, comets, burst explosions — with physics, touch/hover interaction, lifetime animations, trails, and burst emitters.

pub package pub points likes License: MIT Live Demo Buy Me A Coffee

Live Demo · pub.dev · Issues · Contributing


     


Table of Contents


Install

flutter pub add particles_flutter

Quick Start

import 'package:particles_flutter/engine.dart';
import 'package:particles_flutter/shapes.dart';

Particles(
  width: MediaQuery.of(context).size.width,
  height: MediaQuery.of(context).size.height,
  boundType: BoundType.WrapAround,
  particles: List.generate(80, (_) {
    final rng = Random();
    return CircularParticle(
      radius: rng.nextDouble() * 4 + 1,
      color: Colors.white.withValues(alpha: 0.7),
      velocity: Offset(
        (rng.nextDouble() - 0.5) * 60,
        (rng.nextDouble() - 0.5) * 60,
      ),
    );
  }),
)

Features

Particle Shapes

Import: import 'package:particles_flutter/shapes.dart';

Shape Class
Circle CircularParticle(radius, color, velocity)
Rectangle RectangularParticle(width, height, color, velocity)
Rounded Rectangle RoundRectangularParticle(width, height, cornerRadius, ...)
Triangle TriangularParticle(width, height, color, velocity)
Oval OvoidalParticle(width, height, color, velocity, rotationSpeed)
Image ImageParticle(image, width, height, color, velocity)

All shapes support rotationSpeed and all lifetime animation parameters.


Lifetime Animations

All shapes accept these optional parameters. Omit any to keep default behavior.

Color over lifetime

CircularParticle(
  color: Colors.yellow,
  lifetime: 3.0,
  // Two-color transition:
  endColor: Colors.transparent,
  // OR gradient across lifetime:
  colorGradient: [Colors.white, Colors.yellow, Colors.orange, Colors.transparent],
  colorCurve: Curves.linear,
  ...
)

Scale over lifetime

CircularParticle(
  color: Colors.purple,
  lifetime: 2.5,
  startScale: 0.1,
  endScale: 1.8,
  scaleCurve: Curves.easeInOut,
  ...
)

Fade over lifetime

CircularParticle(
  color: Colors.white,
  lifetime: 2.0,
  startOpacity: 1.0,
  endOpacity: 0.0,
  opacityCurve: Curves.easeIn,
  ...
)

Tip: Set both startOpacity: 0.0 and endOpacity: 0.0 for a triangle fade — particles fade in to full opacity at mid-life, then back out. No visible pop on spawn or death.

CircularParticle(
  lifetime: 3.0,
  startOpacity: 0.0,  // fade in from invisible
  endOpacity: 0.0,    // fade out to invisible — triangle curve auto-applied
  ...
)

Particle trails

CircularParticle(
  color: Colors.cyan,
  lifetime: 2.0,
  trailEnabled: true,
  trailLength: 7,   // past positions to draw
  trailFade: true,  // fade older segments
  ...
)

Boundary Types

Value Behaviour
BoundType.None Particles exit the canvas (default)
BoundType.WrapAround Particles reappear from the opposite edge
BoundType.Bounce Particles reflect off edges
Particles(
  boundType: BoundType.WrapAround,
  ...
)

Touch & Hover Interaction

Import: import 'package:particles_flutter/interactions.dart';

Particles(
  interaction: ParticleInteraction(
    awayRadius: 120,
    enableHover: true,
    onTapAnimation: true,
    awayAnimationDuration: Duration(milliseconds: 400),
    awayAnimationCurve: Curves.easeOut,
    hoverRadius: 80,
  ),
  ...
)

Particle Physics

Import: import 'package:particles_flutter/physics.dart';

Particles(
  particlePhysics: ParticlePhysics(gravityScale: 30),
  ...
)

Emission Controller

Spawn particles from a fixed point — great for fountains and fireworks.

Particles(
  boundType: BoundType.None,
  particleEmitter: Emitter(
    startPosition: Offset(width / 2, height / 2),
    startPositionRadius: 10,  // spawn scatter radius
    clusterSize: 10,           // particles per burst
    delay: Duration(milliseconds: 300),
    recycles: false,           // true = loop forever
  ),
  ...
)

Burst Emitter

Fire a fixed number of particles in a single burst — configurable spread, repeat interval, physics, and optional manual controller.

Import: import 'package:particles_flutter/engine.dart';

Spread patterns

Pattern Description
RadialBurst All directions evenly
ConeBurst Within a configurable cone angle
DirectionalBurst One direction with spread
CustomBurst Offset Function(int index, int total) — build your own

One-shot radial explosion

Particles(
  particles: const [],
  width: size.width,
  height: size.height,
  boundType: BoundType.None,
  burstEmitters: [
    BurstEmitter(
      position: (size) => size.center(Offset.zero),
      particleCount: 60,
      pattern: RadialBurst(minSpeed: 150, maxSpeed: 400),
      repeatCount: 1,
      physics: ParticlePhysics(gravityScale: 120),
      particleFactory: (i, total) => CircularParticle(
        radius: 4,
        color: Colors.orange,
        velocity: Offset.zero,
        lifetime: 1.8,
        endOpacity: 0.0,
      ),
    ),
  ],
)

Cone confetti from bottom

BurstEmitter(
  position: (size) => Offset(size.width / 2, size.height),
  particleCount: 50,
  pattern: ConeBurst(
    angle: -pi / 2,    // shoot upward
    spread: pi / 2.5,
    minSpeed: 300,
    maxSpeed: 600,
  ),
  repeatCount: 1,
  positionRadius: 60,
  physics: ParticlePhysics(gravityScale: 200),
  particleFactory: (i, total) => RoundRectangularParticle(
    width: 12, height: 4, cornerRadius: 2,
    color: Colors.pink,
    velocity: Offset.zero,
    rotationSpeed: 3.0,
    lifetime: 2.0,
    endOpacity: 0.0,
  ),
)

Tap-to-burst at touch point

// In your State:
final _ctrl = BurstEmitterController();
Offset _tapPos = Offset.zero;

// In build:
Listener(
  behavior: HitTestBehavior.opaque,
  onPointerDown: (event) {
    _tapPos = event.localPosition;
    _ctrl.trigger();
  },
  child: Particles(
    particles: const [],
    width: size.width,
    height: size.height,
    boundType: BoundType.None,
    burstEmitters: [
      BurstEmitter(
        position: (size) => _tapPos,
        particleCount: 40,
        pattern: RadialBurst(minSpeed: 80, maxSpeed: 240),
        repeatCount: 0,       // 0 = fire only when triggered
        controller: _ctrl,
        physics: ParticlePhysics(gravityScale: 80),
        particleFactory: (i, total) => CircularParticle(
          radius: 3,
          color: Colors.cyan,
          velocity: Offset.zero,
          lifetime: 1.2,
          endScale: 0.0,
          endOpacity: 0.0,
        ),
      ),
    ],
  ),
)

BurstEmitter parameters

Parameter Type Default Description
position Offset Function(Size) required Burst origin
particleCount int required Particles per burst
particleFactory Particle Function(int, int) required Builds each particle
pattern BurstPattern required Velocity spread strategy
initialDelay Duration Duration.zero Delay before first burst
repeatCount int 1 Bursts to fire; 0 = controller-only
repeatInterval Duration Duration.zero Gap between repeats
positionRadius double 0 Scatter radius around position
physics ParticlePhysics? null Gravity for burst particles
enableTrails bool false Trail rendering (expensive at high counts)
controller BurstEmitterController? null Manual trigger
maxPoolSize int 500 Memory ceiling; oldest particles reclaimed when full

Emitter.burst(...) is a shorthand factory that returns a BurstEmitter — use whichever style you prefer.


Example Scenes

✨ Starfield

Particles(
  boundType: BoundType.WrapAround,
  interaction: ParticleInteraction(awayRadius: 120, enableHover: true),
  particles: List.generate(120, (_) => CircularParticle(
    radius: Random().nextDouble() * 3 + 0.5,
    color: Colors.white.withValues(alpha: 0.7),
    velocity: Offset((Random().nextDouble() - 0.5) * 40,
                     (Random().nextDouble() - 0.5) * 40),
  )),
  ...
)

❄️ Snow

Particles(
  boundType: BoundType.WrapAround,
  particlePhysics: ParticlePhysics(gravityScale: 20),
  particles: List.generate(100, (_) => CircularParticle(
    radius: Random().nextDouble() * 6 + 2,
    color: Colors.white.withValues(alpha: 0.8),
    velocity: Offset((Random().nextDouble() - 0.5) * 20,
                     Random().nextDouble() * 15 + 5),
  )),
  ...
)

🎆 Fireworks

Particles(
  boundType: BoundType.None,
  particlePhysics: ParticlePhysics(gravityScale: 45),
  particleEmitter: Emitter(
    startPosition: Offset(width / 2, height / 2),
    startPositionRadius: width * 0.25,
    clusterSize: 15,
    delay: Duration(milliseconds: 300),
  ),
  particles: List.generate(150, (_) {
    final angle = Random().nextDouble() * 2 * pi;
    final speed = Random().nextDouble() * 120 + 60;
    return TriangularParticle(
      width: 6, height: 6,
      color: Colors.orange,
      velocity: Offset(cos(angle) * speed, sin(angle) * speed),
      rotationSpeed: 3.0,
    );
  }),
  ...
)

☄️ Comets — color gradient + trails

Particles(
  boundType: BoundType.WrapAround,
  particles: List.generate(80, (_) {
    final angle = Random().nextDouble() * 2 * pi;
    final speed = Random().nextDouble() * 60 + 40;
    return CircularParticle(
      radius: Random().nextDouble() * 3 + 2,
      color: Colors.white,
      velocity: Offset(cos(angle) * speed, sin(angle) * speed),
      lifetime: Random().nextDouble() * 2.0 + 1.5,
      colorGradient: [Colors.white, Colors.yellow, Colors.orange, Colors.transparent],
      startOpacity: 0.0,
      endOpacity: 0.0,
      trailEnabled: true,
      trailLength: 7,
      trailFade: true,
    );
  }),
  ...
)

🔮 Pulse — scale over lifetime

Particles(
  boundType: BoundType.WrapAround,
  particles: List.generate(60, (_) => CircularParticle(
    radius: 10,
    color: Color(0xFF7C4DFF),
    velocity: Offset((Random().nextDouble() - 0.5) * 20,
                     (Random().nextDouble() - 0.5) * 20),
    lifetime: 2.5,
    startScale: 0.1,
    endScale: 1.8,
    scaleCurve: Curves.easeInOut,
    startOpacity: 0.0,
    endOpacity: 0.0,
  )),
  ...
)

👻 Ghosts — fade in/out

Particles(
  boundType: BoundType.WrapAround,
  particles: List.generate(50, (_) => CircularParticle(
    radius: Random().nextDouble() * 14 + 8,
    color: Color(0xFF69F0AE),
    velocity: Offset((Random().nextDouble() - 0.5) * 15,
                     (Random().nextDouble() - 0.5) * 10),
    lifetime: Random().nextDouble() * 3.0 + 2.0,
    startOpacity: 0.0,
    endOpacity: 0.0,
    startScale: 0.6,
    endScale: 1.2,
    scaleCurve: Curves.easeOut,
  )),
  ...
)

🚀 Rockets — all lifetime features combined

Particles(
  boundType: BoundType.None,
  particlePhysics: ParticlePhysics(gravityScale: 40),
  particles: List.generate(150, (_) {
    final angle = Random().nextDouble() * 2 * pi;
    final speed = Random().nextDouble() * 80 + 80;
    return CircularParticle(
      radius: Random().nextDouble() * 4 + 2,
      color: Colors.yellow,
      velocity: Offset(cos(angle) * speed, sin(angle) * speed),
      lifetime: Random().nextDouble() * 1.0 + 1.2,
      colorGradient: [Colors.white, Colors.yellow, Colors.red, Colors.transparent],
      startScale: 1.0,
      endScale: 0.0,
      scaleCurve: Curves.easeIn,
      startOpacity: 0.0,
      endOpacity: 0.0,
      trailEnabled: true,
      trailLength: 6,
      trailFade: true,
    );
  }),
  ...
)

💥 Burst — tap-to-explode

final ctrl = BurstEmitterController();
Offset tapPos = Offset.zero;

Listener(
  behavior: HitTestBehavior.opaque,
  onPointerDown: (e) { tapPos = e.localPosition; ctrl.trigger(); },
  child: Particles(
    particles: const [],
    width: size.width,
    height: size.height,
    boundType: BoundType.None,
    burstEmitters: [
      BurstEmitter(
        position: (size) => tapPos,
        particleCount: 40,
        pattern: RadialBurst(minSpeed: 100, maxSpeed: 300),
        repeatCount: 0,
        controller: ctrl,
        physics: ParticlePhysics(gravityScale: 80),
        particleFactory: (i, _) => CircularParticle(
          radius: Random().nextDouble() * 3 + 1.5,
          color: Colors.orange,
          velocity: Offset.zero,
          lifetime: 1.2,
          endScale: 0.0,
          endOpacity: 0.0,
        ),
      ),
    ],
  ),
)

Live Demo

See all scenes running live → particles-flutter.vercel.app

 


Changelog

v3.0

  • Breaking: Dart SDK >=3.0.0 required (Flutter 3.10+). Projects on Dart 2.x must upgrade first.
  • BurstEmitter — fire fixed particle counts in radial, cone, directional, or custom spread patterns
  • BurstEmitterController — trigger bursts manually from gestures, game events, or any code
  • Tap-to-burstBurstEmitterController.trigger() + onPointerDown for per-tap explosions
  • Overlap-safe pooling — multiple bursts in flight simultaneously; memory hard-capped at maxPoolSize

v2.1

  • Color over lifetime — smooth two-color or gradient transitions
  • Scale over lifetime — grow/shrink with curve support
  • Fade over lifetime — fade in, out, or triangle (both ends zero = auto mid-peak)
  • Particle trails — motion trails with configurable length and fade
  • Object pooling for ParticleLine — reduced GC pressure on line-connected scenes
  • Performance improvements — touch interaction, physics, and emitter update loops

All releases are fully backward compatible — no changes needed to existing code.


Support

If this package saved you time:

Buy Me A Coffee

Contributing

Bug reports and pull requests welcome.

Contributors

RealEeveahy

Releases

No releases published

Packages

 
 
 

Contributors