Skip to main content

flutter_floating_particles 2.0.0 copy "flutter_floating_particles: ^2.0.0" to clipboard
flutter_floating_particles: ^2.0.0 copied to clipboard

Animated particle effects for Flutter. Snow, rain, confetti, fireworks, hearts and more, with emitters, bursts, physics, touch interaction, images, SVGs and widgets.

example/lib/main.dart

import 'dart:convert';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_floating_particles/flutter_floating_particles.dart';

import 'code_export.dart';
import 'themes/gallery.dart';

void main() {
  runApp(const ParticleDemoApp());
}

class ParticleDemoApp extends StatelessWidget {
  const ParticleDemoApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Particle Effects Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
      ),
      home: const DemoHome(),
    );
  }
}

class DemoHome extends StatefulWidget {
  const DemoHome({super.key});

  @override
  State<DemoHome> createState() => _DemoHomeState();
}

class _DemoHomeState extends State<DemoHome> {
  int _tab = 0;

  static const _pages = [
    ThemesGallery(),
    PresetsPage(),
    PlaygroundPage(),
    InteractivePage(),
    SourcesPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Only the visible page animates
      body: IndexedStack(
        index: _tab,
        children: [
          for (int i = 0; i < _pages.length; i++)
            TickerMode(enabled: i == _tab, child: _pages[i]),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tab,
        onDestinationSelected: (index) => setState(() => _tab = index),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.celebration), label: 'Themes'),
          NavigationDestination(
            icon: Icon(Icons.auto_awesome),
            label: 'Presets',
          ),
          NavigationDestination(icon: Icon(Icons.tune), label: 'Playground'),
          NavigationDestination(
            icon: Icon(Icons.touch_app),
            label: 'Interactive',
          ),
          NavigationDestination(icon: Icon(Icons.image), label: 'Sources'),
        ],
      ),
    );
  }
}

/// Background gradient used behind each preset.
List<Color> _backgroundFor(ParticleEffectType type) {
  return switch (type) {
    ParticleEffectType.snow => const [Color(0xFF0F2027), Color(0xFF2C5364)],
    ParticleEffectType.rain => const [Color(0xFF34495E), Color(0xFF2C3E50)],
    ParticleEffectType.fireAshes => const [
      Color(0xFF2F1B14),
      Color(0xFF8B0000),
    ],
    ParticleEffectType.bubbles => const [Color(0xFF006994), Color(0xFF004B6B)],
    ParticleEffectType.stars => const [Color(0xFF0F0F23), Color(0xFF1A1A2E)],
    ParticleEffectType.hearts => const [Color(0xFF8E2DE2), Color(0xFF4A00E0)],
    ParticleEffectType.confetti => const [Color(0xFF667DB6), Color(0xFF0082C8)],
    ParticleEffectType.fallingLeaves => const [
      Color(0xFF8B4513),
      Color(0xFF654321),
    ],
    ParticleEffectType.fireflies => const [
      Color(0xFF0B1D0F),
      Color(0xFF1E3A24),
    ],
    ParticleEffectType.starfield => const [
      Color(0xFF000000),
      Color(0xFF0B0B1E),
    ],
    ParticleEffectType.fireworks => const [
      Color(0xFF020111),
      Color(0xFF191970),
    ],
    ParticleEffectType.sakura => const [Color(0xFF6A4C93), Color(0xFFF8C8DC)],
    ParticleEffectType.network => const [Color(0xFF0F2027), Color(0xFF203A43)],
    ParticleEffectType.blizzard => const [Color(0xFF1E3C72), Color(0xFF2A5298)],
  };
}

String _label(String camelCase) {
  final words = camelCase.replaceAllMapped(
    RegExp('([A-Z])'),
    (m) => ' ${m[1]}',
  );
  return words[0].toUpperCase() + words.substring(1);
}

Widget _gradient(List<Color> colors, Widget child) => DecoratedBox(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: colors,
    ),
  ),
  child: SizedBox.expand(child: child),
);

// ---------------------------------------------------------------------------
// Presets: every built-in effect, with enable and layer toggles
// ---------------------------------------------------------------------------

class PresetsPage extends StatefulWidget {
  const PresetsPage({super.key});

  @override
  State<PresetsPage> createState() => _PresetsPageState();
}

class _PresetsPageState extends State<PresetsPage> {
  ParticleEffectType _effect = ParticleEffectType.snow;
  bool _enabled = true;
  bool _behind = false;

  @override
  Widget build(BuildContext context) {
    return _gradient(
      _backgroundFor(_effect),
      ParticleEffects(
        config: _effect.config,
        isEnabled: _enabled,
        layer: _behind ? ParticleLayer.background : ParticleLayer.foreground,
        child: SafeArea(
          child: Column(
            children: [
              const SizedBox(height: 20),
              const Text(
                'Particle Effects',
                style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
              ),
              Text('Showing: ${_label(_effect.name)}'),
              SwitchListTile(
                title: const Text('Enable effects'),
                value: _enabled,
                onChanged: (value) => setState(() => _enabled = value),
              ),
              SwitchListTile(
                title: const Text('Paint behind the content'),
                subtitle: const Text('ParticleLayer.background'),
                value: _behind,
                onChanged: (value) => setState(() => _behind = value),
              ),
              Expanded(
                child: GridView.count(
                  padding: const EdgeInsets.all(16),
                  crossAxisCount: 2,
                  mainAxisSpacing: 12,
                  crossAxisSpacing: 12,
                  childAspectRatio: 2.8,
                  children: [
                    for (final effect in ParticleEffectType.values)
                      FilledButton.tonal(
                        onPressed: () => setState(() => _effect = effect),
                        style: FilledButton.styleFrom(
                          backgroundColor: effect == _effect
                              ? Theme.of(context).colorScheme.primary
                              : Colors.white.withValues(alpha: 0.15),
                        ),
                        child: Text(_label(effect.name)),
                      ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Playground: tweak every option live
// ---------------------------------------------------------------------------

class PlaygroundPage extends StatefulWidget {
  const PlaygroundPage({super.key});

  @override
  State<PlaygroundPage> createState() => _PlaygroundPageState();
}

class _PlaygroundPageState extends State<PlaygroundPage> {
  /// The starting point: what the page opens with, or an imported config.
  /// The controls adjust it, and options they don't cover are kept.
  static const _defaults = ParticleConfig(
    particleType: ParticleType.star,
    particleCount: 60,
    maxSize: 10,
    enableGlow: true,
    enableRotation: true,
    gradientColors: [Color(0xFFFFC107), Color(0xFF00BCD4), Color(0xFFFF4081)],
  );

  ParticleConfig _base = _defaults;
  late ParticleType _type;
  late ParticleDirection _direction;
  late ParticleCoverage _coverage;
  late double _count;
  late double _speed;
  late double _wind;
  late double _maxSize;
  late double _edgeFade;
  late bool _glow;
  late bool _rotate;
  late bool _additive;
  late bool _depth;
  late bool _trail;
  late bool _connections;
  late bool _shrink;
  late bool _splash;
  late bool _mixed;
  late int _seed;

  @override
  void initState() {
    super.initState();
    _load(_defaults);
  }

  /// Sets the controls from [config].
  void _load(ParticleConfig config) {
    _base = config;
    // Custom widgets can't come from JSON
    _type = config.particleType == ParticleType.custom
        ? ParticleType.circle
        : config.particleType;
    _direction = config.direction;
    _coverage = config.particleCoverage;
    _count = config.particleCount.toDouble();
    _speed = config.velocityMultiplier;
    _wind = config.wind;
    _maxSize = config.maxSize;
    _edgeFade = config.edgeFade;
    _glow = config.enableGlow;
    _rotate = config.enableRotation;
    _additive = config.blendMode == BlendMode.plus;
    _depth = config.depthEffect > 0;
    _trail = config.trail != null;
    _connections = config.connections != null;
    _shrink = config.lifecycle != null;
    _splash = config.splash != null;
    _mixed = config.particleTypes != null;
    _seed = config.seed ?? 0;
  }

  ParticleConfig get _config {
    final base = _base;
    final json = base.toJson()
      ..addAll({
        'particleType': _type.name,
        'direction': _direction.name,
        'particleCoverage': _coverage.name,
        'particleCount': _count.round(),
        'minSize': identical(base, _defaults)
            ? max(1.0, _maxSize / 3)
            : min(base.minSize, _maxSize),
        'maxSize': _maxSize,
        'velocityMultiplier': _speed,
        'wind': _wind,
        'edgeFade': _edgeFade,
        'enableGlow': _glow,
        'enableRotation': _rotate,
        'blendMode': _additive
            ? BlendMode.plus.name
            : base.blendMode == BlendMode.plus
            ? BlendMode.srcOver.name
            : base.blendMode.name,
        'seed': _seed,
        'depthEffect': _depth
            ? (base.depthEffect > 0 ? base.depthEffect : 0.8)
            : 0.0,
      });
    // Toggles keep the imported options, or add sensible ones
    void toggle(String key, bool on, Object? fallback) {
      on ? json[key] ??= fallback : json.remove(key);
    }

    toggle('particleTypes', _mixed, const ['star', 'heart', 'sparkle']);
    toggle('trail', _trail, const ParticleTrail(length: 8).toJson());
    toggle('connections', _connections, const ParticleConnections().toJson());
    toggle('lifecycle', _shrink, ParticleLifecycle.shrink.toJson());
    toggle('splash', _splash, const ParticleSplash().toJson());
    return ParticleConfig.fromJson(json).copyWith(
      // Used when the type is ParticleType.path
      customPath: _lightningBolt,
    );
  }

  Future<void> _import() async {
    final config = await showModalBottomSheet<ParticleConfig>(
      context: context,
      isScrollControlled: true,
      builder: (context) => const _ImportSheet(),
    );
    if (config == null || !mounted) return;
    setState(() => _load(config));
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(const SnackBar(content: Text('Config imported')));
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        children: [
          Expanded(
            child: _gradient(const [
              Color(0xFF141E30),
              Color(0xFF243B55),
            ], ParticleEffects(config: _config)),
          ),
          SizedBox(
            height: 300,
            child: ListView(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              children: [
                Wrap(
                  spacing: 12,
                  children: [
                    DropdownButton<ParticleType>(
                      value: _type,
                      items: [
                        for (final type in ParticleType.values)
                          // Images only when an imported config has one
                          if (type != ParticleType.custom &&
                              (type != ParticleType.image ||
                                  _type == ParticleType.image))
                            DropdownMenuItem(
                              value: type,
                              child: Text(_label(type.name)),
                            ),
                      ],
                      onChanged: (value) => setState(() => _type = value!),
                    ),
                    DropdownButton<ParticleDirection>(
                      value: _direction,
                      items: [
                        for (final direction in ParticleDirection.values)
                          DropdownMenuItem(
                            value: direction,
                            child: Text(_label(direction.name)),
                          ),
                      ],
                      onChanged: (value) => setState(() => _direction = value!),
                    ),
                    DropdownButton<ParticleCoverage>(
                      value: _coverage,
                      items: [
                        for (final coverage in ParticleCoverage.values)
                          DropdownMenuItem(
                            value: coverage,
                            child: Text('Coverage ${_label(coverage.name)}'),
                          ),
                      ],
                      onChanged: (value) => setState(() => _coverage = value!),
                    ),
                  ],
                ),
                _slider('Count', _count, 0, 500, (v) => _count = v),
                _slider('Speed', _speed, 0.1, 3, (v) => _speed = v),
                _slider('Wind', _wind, -1, 1, (v) => _wind = v),
                _slider('Max size', _maxSize, 2, 40, (v) => _maxSize = v),
                _slider('Edge fade', _edgeFade, 0, 0.5, (v) => _edgeFade = v),
                Wrap(
                  spacing: 8,
                  children: [
                    FilterChip(
                      label: const Text('Glow'),
                      selected: _glow,
                      onSelected: (v) => setState(() => _glow = v),
                    ),
                    FilterChip(
                      label: const Text('Rotate'),
                      selected: _rotate,
                      onSelected: (v) => setState(() => _rotate = v),
                    ),
                    FilterChip(
                      label: const Text('Additive blend'),
                      selected: _additive,
                      onSelected: (v) => setState(() => _additive = v),
                    ),
                    for (final (label, value, set) in [
                      ('Mix shapes', _mixed, (bool v) => _mixed = v),
                      ('Depth', _depth, (bool v) => _depth = v),
                      ('Trail', _trail, (bool v) => _trail = v),
                      (
                        'Connections',
                        _connections,
                        (bool v) => _connections = v,
                      ),
                      ('Shrink over life', _shrink, (bool v) => _shrink = v),
                      ('Splash', _splash, (bool v) => _splash = v),
                    ])
                      FilterChip(
                        label: Text(label),
                        selected: value,
                        onSelected: (v) => setState(() => set(v)),
                      ),
                    ActionChip(
                      label: const Text('New layout (seed)'),
                      onPressed: () => setState(() => _seed++),
                    ),
                    if (!identical(_base, _defaults))
                      ActionChip(
                        avatar: const Icon(Icons.restart_alt, size: 18),
                        label: const Text('Reset'),
                        onPressed: () => setState(() => _load(_defaults)),
                      ),
                  ],
                ),
                const SizedBox(height: 8),
                FilledButton.icon(
                  onPressed: () => _showExport(context, _config),
                  icon: const Icon(Icons.code),
                  label: const Text('Export as Dart code or JSON'),
                ),
                const SizedBox(height: 8),
                OutlinedButton.icon(
                  onPressed: _import,
                  icon: const Icon(Icons.file_download_outlined),
                  label: const Text('Import JSON'),
                ),
                const SizedBox(height: 16),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _slider(
    String label,
    double value,
    double lower,
    double upper,
    void Function(double) onChanged,
  ) {
    return Row(
      children: [
        SizedBox(width: 80, child: Text(label)),
        Expanded(
          child: Slider(
            value: value,
            // Widened for imported values outside the usual range
            min: min(lower, value),
            max: max(upper, value),
            onChanged: (v) => setState(() => onChanged(v)),
          ),
        ),
        SizedBox(width: 44, child: Text(value.toStringAsFixed(1))),
      ],
    );
  }
}

/// Shows [config] as Dart code and JSON, ready to copy.
void _showExport(BuildContext context, ParticleConfig config) {
  final dart = configToDart(config);
  final json = const JsonEncoder.withIndent('  ').convert(config.toJson());
  showModalBottomSheet<void>(
    context: context,
    isScrollControlled: true,
    builder: (context) => DefaultTabController(
      length: 2,
      child: SizedBox(
        height: MediaQuery.sizeOf(context).height * 0.7,
        child: Column(
          children: [
            const TabBar(
              tabs: [
                Tab(text: 'Dart'),
                Tab(text: 'JSON'),
              ],
            ),
            Expanded(
              child: TabBarView(
                children: [
                  for (final code in [dart, json])
                    Stack(
                      children: [
                        SingleChildScrollView(
                          padding: const EdgeInsets.all(16),
                          child: SelectableText(
                            code,
                            style: const TextStyle(
                              fontFamily: 'monospace',
                              fontSize: 13,
                            ),
                          ),
                        ),
                        Positioned(
                          right: 12,
                          top: 12,
                          child: IconButton.filledTonal(
                            tooltip: 'Copy',
                            icon: const Icon(Icons.copy),
                            onPressed: () {
                              Clipboard.setData(ClipboardData(text: code));
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('Copied')),
                              );
                            },
                          ),
                        ),
                      ],
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

/// Paste JSON from the export sheet (or anywhere else) to load it.
class _ImportSheet extends StatefulWidget {
  const _ImportSheet();

  @override
  State<_ImportSheet> createState() => _ImportSheetState();
}

class _ImportSheetState extends State<_ImportSheet> {
  final _text = TextEditingController();
  String? _error;

  @override
  void dispose() {
    _text.dispose();
    super.dispose();
  }

  Future<void> _paste() async {
    final data = await Clipboard.getData(Clipboard.kTextPlain);
    if (data?.text case final text?) {
      setState(() {
        _text.text = text;
        _error = null;
      });
    }
  }

  void _submit() {
    try {
      final json = jsonDecode(_text.text);
      if (json is! Map<String, Object?>) {
        setState(() => _error = 'Expected a JSON object: { ... }');
        return;
      }
      Navigator.of(context).pop(ParticleConfig.fromJson(json));
    } on FormatException catch (error) {
      setState(() => _error = 'Not valid JSON: ${error.message}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.fromLTRB(
        16,
        16,
        16,
        16 + MediaQuery.viewInsetsOf(context).bottom,
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('Import JSON', style: Theme.of(context).textTheme.titleLarge),
          const SizedBox(height: 4),
          const Text(
            'Paste a config exported from here or made with '
            'ParticleConfig.toJson(). Missing options use their defaults.',
          ),
          const SizedBox(height: 12),
          TextField(
            controller: _text,
            minLines: 6,
            maxLines: 12,
            style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
            decoration: InputDecoration(
              border: const OutlineInputBorder(),
              hintText: '{\n  "particleType": "snowflake",\n  ...\n}',
              errorText: _error,
              errorMaxLines: 3,
            ),
            onChanged: (_) {
              if (_error != null) setState(() => _error = null);
            },
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              TextButton.icon(
                onPressed: _paste,
                icon: const Icon(Icons.content_paste),
                label: const Text('Paste'),
              ),
              const Spacer(),
              FilledButton(onPressed: _submit, child: const Text('Import')),
            ],
          ),
        ],
      ),
    );
  }
}

/// A custom shape for [ParticleType.path]; any coordinates work.
final Path _lightningBolt = Path()
  ..moveTo(14, 0)
  ..lineTo(2, 14)
  ..lineTo(10, 14)
  ..lineTo(6, 24)
  ..lineTo(18, 9)
  ..lineTo(10, 9)
  ..close();

// ---------------------------------------------------------------------------
// Interactive: controller, bursts and pointer interaction
// ---------------------------------------------------------------------------

class InteractivePage extends StatefulWidget {
  const InteractivePage({super.key});

  @override
  State<InteractivePage> createState() => _InteractivePageState();
}

class _InteractivePageState extends State<InteractivePage> {
  final _ambient = ParticleController();
  final _confetti = ParticleController();
  final _fireworks = ParticleController();
  ParticleInteraction _interaction = ParticleInteraction.repel;

  static const _modes = {
    'Repel': ParticleInteraction.repel,
    'Attract': ParticleInteraction.attract,
    'Tap burst': ParticleInteraction.tapToBurst,
    'Pop': ParticleInteraction.pop,
    'Paint': ParticleInteraction.paint,
  };

  @override
  void dispose() {
    _ambient.dispose();
    _confetti.dispose();
    _fireworks.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return _gradient(
      const [Color(0xFF1F1C2C), Color(0xFF3A3560)],
      // Stacked effects: fireworks at the back, interactive fireflies in the
      // middle, confetti bursts on top. Bursts use burst-only configs
      // (particleCount: 0).
      ParticleEffects(
        controller: _fireworks,
        layer: ParticleLayer.background,
        config: ParticleConfig.fireworks.copyWith(emitters: const []),
        child: ParticleEffects(
          controller: _confetti,
          config: ParticleConfig.confetti.copyWith(particleCount: 0),
          child: ParticleEffects(
            controller: _ambient,
            config: ParticleConfig.fireflies.copyWith(particleCount: 80),
            interaction: _interaction,
            child: SafeArea(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  children: [
                    const Text(
                      'Move, tap or drag on the screen',
                      style: TextStyle(fontSize: 22),
                    ),
                    const SizedBox(height: 12),
                    Wrap(
                      spacing: 8,
                      runSpacing: 8,
                      alignment: WrapAlignment.center,
                      children: [
                        for (final MapEntry(key: label, value: mode)
                            in _modes.entries)
                          ChoiceChip(
                            label: Text(label),
                            selected: _interaction == mode,
                            onSelected: (_) =>
                                setState(() => _interaction = mode),
                          ),
                      ],
                    ),
                    const Spacer(),
                    ListenableBuilder(
                      listenable: _ambient,
                      builder: (context, _) => Row(
                        children: [
                          const Text('Speed'),
                          Expanded(
                            child: Slider(
                              value: _ambient.timeScale,
                              min: 0,
                              max: 3,
                              onChanged: (v) => _ambient.timeScale = v,
                            ),
                          ),
                          Text('${_ambient.timeScale.toStringAsFixed(1)}x'),
                        ],
                      ),
                    ),
                    Wrap(
                      spacing: 12,
                      runSpacing: 12,
                      alignment: WrapAlignment.center,
                      children: [
                        ListenableBuilder(
                          listenable: _ambient,
                          builder: (context, _) => FilledButton.icon(
                            onPressed: _ambient.toggle,
                            icon: Icon(
                              _ambient.isPaused
                                  ? Icons.play_arrow
                                  : Icons.pause,
                            ),
                            label: Text(_ambient.isPaused ? 'Resume' : 'Pause'),
                          ),
                        ),
                        FilledButton.icon(
                          onPressed: _ambient.reset,
                          icon: const Icon(Icons.replay),
                          label: const Text('Reset'),
                        ),
                        FilledButton.icon(
                          onPressed: () => _confetti.confettiCannon(),
                          icon: const Icon(Icons.celebration),
                          label: const Text('Confetti'),
                        ),
                        FilledButton.icon(
                          onPressed: () => _confetti.explode(count: 80),
                          icon: const Icon(Icons.flare),
                          label: const Text('Explode'),
                        ),
                        FilledButton.icon(
                          onPressed: () => _fireworks.fireworks(shells: 4),
                          icon: const Icon(Icons.auto_awesome),
                          label: const Text('Fireworks'),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Sources: images, SVG, ImageProvider, widgets and paths
// ---------------------------------------------------------------------------

class SourcesPage extends StatefulWidget {
  const SourcesPage({super.key});

  @override
  State<SourcesPage> createState() => _SourcesPageState();
}

class _SourcesPageState extends State<SourcesPage> {
  static final _sources = <String, ParticleConfig>{
    'Asset PNG': const ParticleConfig(
      particleType: ParticleType.image,
      imagePath: 'assets/icon_flutter.png',
      particleCount: 30,
      minSize: 12,
      maxSize: 28,
      enableRotation: true,
      velocityMultiplier: 0.6,
    ),
    'Network PNG': const ParticleConfig(
      particleType: ParticleType.image,
      imagePath: 'https://img.icons8.com/fluency/48/filled-like--v1.png',
      particleCount: 30,
      minSize: 12,
      maxSize: 24,
      velocityMultiplier: 0.6,
    ),
    'Asset SVG': const ParticleConfig(
      particleType: ParticleType.image,
      imagePath: 'assets/heart.svg',
      direction: ParticleDirection.bottomToTop,
      particleCount: 30,
      minSize: 10,
      maxSize: 26,
      velocityMultiplier: 0.5,
    ),
    'ImageProvider + tint': const ParticleConfig(
      particleType: ParticleType.image,
      image: AssetImage('assets/icon_flutter.png'),
      gradientColors: [Colors.amber, Colors.lightGreenAccent, Colors.cyan],
      particleCount: 40,
      minSize: 10,
      maxSize: 22,
      enableRotation: true,
    ),
    'Custom widget': const ParticleConfig(
      particleType: ParticleType.custom,
      customParticle: Icon(Icons.ac_unit, color: Colors.white),
      particleCount: 40,
      minSize: 10,
      maxSize: 30,
      enableRotation: true,
      velocityMultiplier: 0.5,
    ),
    'Custom path': ParticleConfig(
      particleType: ParticleType.path,
      customPath: _lightningBolt,
      particleColor: Colors.yellowAccent,
      enableGlow: true,
      particleCount: 30,
      minSize: 10,
      maxSize: 24,
      direction: ParticleDirection.diagonal,
    ),
  };

  String _selected = _sources.keys.first;

  @override
  Widget build(BuildContext context) {
    return _gradient(
      const [Color(0xFF16222A), Color(0xFF3A6073)],
      ParticleEffects(
        config: _sources[_selected]!,
        loadingWidget: const CircularProgressIndicator(),
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                for (final name in _sources.keys)
                  ChoiceChip(
                    label: Text(name),
                    selected: name == _selected,
                    onSelected: (_) => setState(() => _selected = name),
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
11
likes
160
points
475
downloads

Documentation

API reference

Publisher

verified publisherniteshmandal1.com.np

Weekly Downloads

Animated particle effects for Flutter. Snow, rain, confetti, fireworks, hearts and more, with emitters, bursts, physics, touch interaction, images, SVGs and widgets.

Repository (GitHub)
View/report issues

Topics

#particles #animation #effects #confetti #ui

License

MIT (license)

Dependencies

flutter, flutter_svg

More

Packages that depend on flutter_floating_particles