ambient_effects_container 0.1.0 copy "ambient_effects_container: ^0.1.0" to clipboard
ambient_effects_container: ^0.1.0 copied to clipboard

A reusable Flutter ambient background container with rain, snow, leaf, and sakura particle effects.

example/lib/main.dart

import 'package:ambient_effects_container/ambient_effects_container.dart';
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Ambient Effects Container',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF496A81),
          brightness: Brightness.light,
        ),
        scaffoldBackgroundColor: Colors.transparent,
        useMaterial3: true,
      ),
      home: const AmbientEffectsDemoPage(),
    );
  }
}

enum DemoEffectKind { rain, snow, leaf, sakura }

enum DemoIntensity { light, medium, heavy }

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

  @override
  State<AmbientEffectsDemoPage> createState() => _AmbientEffectsDemoPageState();
}

class _AmbientEffectsDemoPageState extends State<AmbientEffectsDemoPage> {
  DemoEffectKind _effectKind = DemoEffectKind.rain;
  DemoIntensity _intensity = DemoIntensity.medium;
  PerformanceMode _performanceMode = PerformanceMode.balanced;
  EffectRenderStyle _renderStyle = EffectRenderStyle.layered;
  bool _enabled = true;
  double _speed = 1.0;
  double _opacity = 1.0;
  double _particleSliderValue = 0;

  int? get _particleCountOverride {
    final value = _particleSliderValue.round();
    return value <= 0 ? null : value;
  }

  AmbientEffectConfig _buildEffect({double speedMultiplier = 1.0}) {
    switch (_effectKind) {
      case DemoEffectKind.rain:
        return RainEffect(
          intensity: _mapRainIntensity(_intensity),
          speed: _speed * speedMultiplier,
          opacity: 0.55 * _opacity,
          particleCount: _particleCountOverride,
          color: const Color(0xFFCFE5FF),
          angle: -0.24,
          wind: 0.15,
          strokeWidth: _intensity == DemoIntensity.heavy ? 1.4 : 1.1,
        );
      case DemoEffectKind.snow:
        return SnowEffect(
          intensity: _mapSnowIntensity(_intensity),
          speed: _speed * speedMultiplier,
          opacity: 0.88 * _opacity,
          particleCount: _particleCountOverride,
          minSize: 2,
          maxSize: _intensity == DemoIntensity.heavy ? 8 : 6,
          drift: 1.1,
          color: const Color(0xFFFDFEFF),
        );
      case DemoEffectKind.leaf:
        return LeafEffect(
          intensity: _mapLeafIntensity(_intensity),
          speed: _speed * speedMultiplier,
          opacity: 0.92 * _opacity,
          particleCount: _particleCountOverride,
          minSize: 10,
          maxSize: _intensity == DemoIntensity.heavy ? 24 : 20,
          sway: 1.1,
          rotationSpeed: 1.05,
          palette: const <Color>[
            Color(0xFFD86A2D),
            Color(0xFFCA4C1D),
            Color(0xFFE5A056),
            Color(0xFFA93D1D),
          ],
        );
      case DemoEffectKind.sakura:
        return SakuraEffect(
          intensity: _mapSakuraIntensity(_intensity),
          speed: _speed * speedMultiplier,
          opacity: 0.94 * _opacity,
          particleCount: _particleCountOverride,
          minSize: 8,
          maxSize: _intensity == DemoIntensity.heavy ? 20 : 17,
          sway: 1.18,
          rotationSpeed: 0.92,
          palette: const <Color>[
            Color(0xFFFFD8E6),
            Color(0xFFFFC7DD),
            Color(0xFFFFE3EE),
            Color(0xFFF7B7CF),
          ],
        );
    }
  }

  Color _sceneColor() {
    return switch (_effectKind) {
      DemoEffectKind.rain => const Color(0xFF213547),
      DemoEffectKind.snow => const Color(0xFF99B7C8),
      DemoEffectKind.leaf => const Color(0xFF7B5135),
      DemoEffectKind.sakura => const Color(0xFF88576B),
    };
  }

  LinearGradient _panelGradient() {
    return switch (_effectKind) {
      DemoEffectKind.rain => const LinearGradient(
        colors: <Color>[Color(0xFFD3E4F0), Color(0xFFF6F9FB)],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      DemoEffectKind.snow => const LinearGradient(
        colors: <Color>[Color(0xFFE6F4FA), Color(0xFFFDFEFF)],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      DemoEffectKind.leaf => const LinearGradient(
        colors: <Color>[Color(0xFFF3D9BF), Color(0xFFFBF2E8)],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      DemoEffectKind.sakura => const LinearGradient(
        colors: <Color>[Color(0xFFFFE0EA), Color(0xFFFFF4F8)],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    };
  }

  @override
  Widget build(BuildContext context) {
    final effect = _buildEffect();

    return AmbientEffectsContainer(
      effect: effect,
      enabled: _enabled,
      performanceMode: _performanceMode,
      renderStyle: _renderStyle,
      backgroundColor: _sceneColor(),
      child: Scaffold(
        backgroundColor: Colors.transparent,
        body: SafeArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(20),
            child: Center(
              child: ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 1100),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    _buildHero(context),
                    const SizedBox(height: 20),
                    LayoutBuilder(
                      builder:
                          (BuildContext context, BoxConstraints constraints) {
                            final isWide = constraints.maxWidth >= 860;
                            if (isWide) {
                              return Row(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  Expanded(
                                    flex: 3,
                                    child: _buildPreviewPanel(effect),
                                  ),
                                  const SizedBox(width: 20),
                                  Expanded(
                                    flex: 2,
                                    child: _buildControlPanel(context),
                                  ),
                                ],
                              );
                            }

                            return Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: <Widget>[
                                _buildPreviewPanel(effect),
                                const SizedBox(height: 20),
                                _buildControlPanel(context),
                              ],
                            );
                          },
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildHero(BuildContext context) {
    final theme = Theme.of(context);

    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: _panelGradient(),
        borderRadius: BorderRadius.circular(28),
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Color(0x22000000),
            blurRadius: 28,
            offset: Offset(0, 18),
          ),
        ],
      ),
      child: Padding(
        padding: const EdgeInsets.all(28),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'Ambient Effects Container',
              style: theme.textTheme.headlineMedium?.copyWith(
                fontWeight: FontWeight.w700,
                color: const Color(0xFF1F2A33),
              ),
            ),
            const SizedBox(height: 12),
            Text(
              'A reusable animated background container that can wrap any widget. This demo shows both a full-page background and a local card background so you can quickly compare particle density, speed, and opacity.',
              style: theme.textTheme.bodyLarge?.copyWith(
                height: 1.5,
                color: const Color(0xFF405261),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPreviewPanel(AmbientEffectConfig effect) {
    return Column(
      children: <Widget>[
        _PreviewCard(
          title: 'Full-Screen Background Demo',
          subtitle:
              'Keep the Scaffold background transparent so the effect layer stays behind your content.',
          child: SizedBox(
            height: 300,
            child: DecoratedBox(
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(24),
                gradient: LinearGradient(
                  colors: <Color>[
                    Colors.white.withAlpha(41),
                    Colors.white.withAlpha(10),
                  ],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
                border: Border.all(color: Colors.white.withAlpha(46)),
              ),
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      _headlineText(),
                      style: const TextStyle(
                        fontSize: 26,
                        fontWeight: FontWeight.w700,
                        color: Colors.white,
                      ),
                    ),
                    const SizedBox(height: 10),
                    const Text(
                      'You can wrap the entire page with AmbientEffectsContainer and keep the content layer organized the same way as before.',
                      style: TextStyle(
                        fontSize: 15,
                        height: 1.45,
                        color: Color(0xFFE5EDF2),
                      ),
                    ),
                    const Spacer(),
                    Wrap(
                      spacing: 12,
                      runSpacing: 12,
                      children: <Widget>[
                        _MetricChip(label: 'Effect', value: _effectKind.name),
                        _MetricChip(
                          label: 'Performance',
                          value: _performanceMode.name,
                        ),
                        _MetricChip(label: 'Style', value: _renderStyle.name),
                        _MetricChip(
                          label: 'Particles',
                          value: _particleCountOverride?.toString() ?? 'auto',
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ),
        const SizedBox(height: 20),
        _PreviewCard(
          title: 'Local Container Demo',
          subtitle:
              'Local regions scale particle density by actual size so smaller cards do not look overcrowded.',
          child: AmbientEffectsContainer(
            effect: effect,
            enabled: _enabled,
            performanceMode: _performanceMode,
            renderStyle: _renderStyle,
            backgroundColor: Colors.white.withAlpha(20),
            child: Container(
              height: 230,
              padding: const EdgeInsets.all(24),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(24),
                border: Border.all(color: Colors.white.withAlpha(46)),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  const Text(
                    'Now Playing',
                    style: TextStyle(
                      fontSize: 13,
                      letterSpacing: 1.2,
                      color: Color(0xFFDCE8EF),
                    ),
                  ),
                  const SizedBox(height: 10),
                  const Text(
                    'Silent Horizon',
                    style: TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.w700,
                      color: Colors.white,
                    ),
                  ),
                  const SizedBox(height: 6),
                  const Text(
                    'A compact demo card with the same effect pipeline.',
                    style: TextStyle(fontSize: 14, color: Color(0xFFD7E4EC)),
                  ),
                  const Spacer(),
                  Row(
                    children: const <Widget>[
                      Icon(Icons.graphic_eq, color: Colors.white),
                      SizedBox(width: 12),
                      Expanded(
                        child: LinearProgressIndicator(
                          value: 0.62,
                          color: Colors.white,
                          backgroundColor: Color(0x55FFFFFF),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildControlPanel(BuildContext context) {
    final theme = Theme.of(context);

    return DecoratedBox(
      decoration: BoxDecoration(
        color: Colors.white.withAlpha(230),
        borderRadius: BorderRadius.circular(24),
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Color(0x14000000),
            blurRadius: 24,
            offset: Offset(0, 16),
          ),
        ],
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              'Control Panel',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.w700,
              ),
            ),
            const SizedBox(height: 16),
            _SectionTitle(title: 'Effect Type'),
            SegmentedButton<DemoEffectKind>(
              segments: const <ButtonSegment<DemoEffectKind>>[
                ButtonSegment(value: DemoEffectKind.rain, label: Text('Rain')),
                ButtonSegment(value: DemoEffectKind.snow, label: Text('Snow')),
                ButtonSegment(value: DemoEffectKind.leaf, label: Text('Leaf')),
                ButtonSegment(
                  value: DemoEffectKind.sakura,
                  label: Text('Sakura'),
                ),
              ],
              selected: <DemoEffectKind>{_effectKind},
              onSelectionChanged: (Set<DemoEffectKind> value) {
                setState(() {
                  _effectKind = value.first;
                });
              },
            ),
            const SizedBox(height: 16),
            _SectionTitle(title: 'Intensity'),
            SegmentedButton<DemoIntensity>(
              segments: const <ButtonSegment<DemoIntensity>>[
                ButtonSegment(value: DemoIntensity.light, label: Text('Light')),
                ButtonSegment(
                  value: DemoIntensity.medium,
                  label: Text('Medium'),
                ),
                ButtonSegment(value: DemoIntensity.heavy, label: Text('Heavy')),
              ],
              selected: <DemoIntensity>{_intensity},
              onSelectionChanged: (Set<DemoIntensity> value) {
                setState(() {
                  _intensity = value.first;
                });
              },
            ),
            const SizedBox(height: 16),
            _SectionTitle(title: 'Performance Mode'),
            SegmentedButton<PerformanceMode>(
              segments: const <ButtonSegment<PerformanceMode>>[
                ButtonSegment(value: PerformanceMode.low, label: Text('Low')),
                ButtonSegment(
                  value: PerformanceMode.balanced,
                  label: Text('Balanced'),
                ),
                ButtonSegment(value: PerformanceMode.high, label: Text('High')),
              ],
              selected: <PerformanceMode>{_performanceMode},
              onSelectionChanged: (Set<PerformanceMode> value) {
                setState(() {
                  _performanceMode = value.first;
                });
              },
            ),
            const SizedBox(height: 16),
            _SectionTitle(title: 'Render Style'),
            SegmentedButton<EffectRenderStyle>(
              segments: const <ButtonSegment<EffectRenderStyle>>[
                ButtonSegment(
                  value: EffectRenderStyle.flat,
                  label: Text('Flat'),
                ),
                ButtonSegment(
                  value: EffectRenderStyle.layered,
                  label: Text('Layered'),
                ),
                ButtonSegment(
                  value: EffectRenderStyle.cinematic,
                  label: Text('Cinematic'),
                ),
              ],
              selected: <EffectRenderStyle>{_renderStyle},
              onSelectionChanged: (Set<EffectRenderStyle> value) {
                setState(() {
                  _renderStyle = value.first;
                });
              },
            ),
            const SizedBox(height: 16),
            SwitchListTile.adaptive(
              contentPadding: EdgeInsets.zero,
              value: _enabled,
              title: const Text('Enable Animation'),
              subtitle: const Text(
                'Turning this off stops the controller to avoid unnecessary repaints.',
              ),
              onChanged: (bool value) {
                setState(() {
                  _enabled = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _ValueSlider(
              label: 'Speed Multiplier',
              value: _speed,
              min: 0.5,
              max: 1.8,
              divisions: 13,
              valueText: _speed.toStringAsFixed(2),
              onChanged: (double value) {
                setState(() {
                  _speed = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _ValueSlider(
              label: 'Opacity Multiplier',
              value: _opacity,
              min: 0.35,
              max: 1.0,
              divisions: 13,
              valueText: _opacity.toStringAsFixed(2),
              onChanged: (double value) {
                setState(() {
                  _opacity = value;
                });
              },
            ),
            const SizedBox(height: 12),
            _ValueSlider(
              label: 'Particle Count Override',
              value: _particleSliderValue,
              min: 0,
              max: 160,
              divisions: 16,
              valueText: _particleCountOverride?.toString() ?? 'auto',
              onChanged: (double value) {
                setState(() {
                  _particleSliderValue = value;
                });
              },
            ),
          ],
        ),
      ),
    );
  }

  String _headlineText() {
    return switch (_effectKind) {
      DemoEffectKind.rain => 'Rain over the bay',
      DemoEffectKind.snow => 'Snow above the ridge',
      DemoEffectKind.leaf => 'Leaves across the trail',
      DemoEffectKind.sakura => 'Petals over the garden',
    };
  }

  RainIntensity _mapRainIntensity(DemoIntensity intensity) {
    return switch (intensity) {
      DemoIntensity.light => RainIntensity.light,
      DemoIntensity.medium => RainIntensity.medium,
      DemoIntensity.heavy => RainIntensity.heavy,
    };
  }

  SnowIntensity _mapSnowIntensity(DemoIntensity intensity) {
    return switch (intensity) {
      DemoIntensity.light => SnowIntensity.light,
      DemoIntensity.medium => SnowIntensity.medium,
      DemoIntensity.heavy => SnowIntensity.heavy,
    };
  }

  LeafIntensity _mapLeafIntensity(DemoIntensity intensity) {
    return switch (intensity) {
      DemoIntensity.light => LeafIntensity.light,
      DemoIntensity.medium => LeafIntensity.medium,
      DemoIntensity.heavy => LeafIntensity.heavy,
    };
  }

  SakuraIntensity _mapSakuraIntensity(DemoIntensity intensity) {
    return switch (intensity) {
      DemoIntensity.light => SakuraIntensity.light,
      DemoIntensity.medium => SakuraIntensity.medium,
      DemoIntensity.heavy => SakuraIntensity.heavy,
    };
  }
}

class _PreviewCard extends StatelessWidget {
  const _PreviewCard({
    required this.title,
    required this.subtitle,
    required this.child,
  });

  final String title;
  final String subtitle;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Colors.white.withAlpha(15),
        borderRadius: BorderRadius.circular(28),
        border: Border.all(color: Colors.white.withAlpha(36)),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              title,
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.w700,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              subtitle,
              style: const TextStyle(
                fontSize: 14,
                height: 1.45,
                color: Color(0xFFD6E2E9),
              ),
            ),
            const SizedBox(height: 16),
            child,
          ],
        ),
      ),
    );
  }
}

class _MetricChip extends StatelessWidget {
  const _MetricChip({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Colors.white.withAlpha(41),
        borderRadius: BorderRadius.circular(999),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
        child: Text(
          '$label: $value',
          style: const TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.w600,
          ),
        ),
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle({required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        title,
        style: Theme.of(
          context,
        ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
      ),
    );
  }
}

class _ValueSlider extends StatelessWidget {
  const _ValueSlider({
    required this.label,
    required this.value,
    required this.min,
    required this.max,
    required this.divisions,
    required this.valueText,
    required this.onChanged,
  });

  final String label;
  final double value;
  final double min;
  final double max;
  final int divisions;
  final String valueText;
  final ValueChanged<double> onChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Text(label, style: Theme.of(context).textTheme.titleSmall),
            const Spacer(),
            Text(valueText),
          ],
        ),
        Slider(
          value: value,
          min: min,
          max: max,
          divisions: divisions,
          label: valueText,
          onChanged: onChanged,
        ),
      ],
    );
  }
}
3
likes
160
points
50
downloads

Documentation

API reference

Publisher

verified publisherblog.zyaire.top

Weekly Downloads

A reusable Flutter ambient background container with rain, snow, leaf, and sakura particle effects.

Repository (GitHub)
View/report issues

Topics

#flutter #animation #particles #background #effects

License

MIT (license)

Dependencies

flutter, path_drawing

More

Packages that depend on ambient_effects_container