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

A reusable Flutter ambient background container with rain, snow, leaf, petal, and firefly 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.white,
        useMaterial3: true,
      ),
      home: const AmbientEffectsDemoHomePage(),
    );
  }
}

enum DemoEffectKind { rain, snow, leaf, sakura, wisteria, firefly }

enum DemoIntensity { light, medium, heavy }

/// 首页卡片和详情页共用同一套效果构建逻辑。
///
/// 这样做的原因不是“为了抽象而抽象”,而是避免首页看起来是一套参数、
/// 详情页调起来又是另一套参数。后续继续新增效果时,只需要补这一处映射,
/// demo 的维护成本会低很多。
AmbientEffectConfig _buildDemoEffect(
  DemoEffectKind kind,
  DemoIntensity intensity, {
  required double speed,
  required double opacity,
  required int? particleCount,
}) {
  switch (kind) {
    case DemoEffectKind.rain:
      return RainEffect(
        intensity: _mapRainIntensity(intensity),
        speed: speed,
        opacity: 0.55 * opacity,
        particleCount: particleCount,
        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,
        opacity: 0.88 * opacity,
        particleCount: particleCount,
        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,
        opacity: 0.92 * opacity,
        particleCount: particleCount,
        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,
        opacity: 0.94 * opacity,
        particleCount: particleCount,
        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),
        ],
      );
    case DemoEffectKind.wisteria:
      return PetalEffect(
        style: PetalStyle.wisteria,
        intensity: _mapPetalIntensity(intensity),
        speed: speed * 0.78,
        opacity: 0.9 * opacity,
        particleCount: particleCount,
        minSize: 7,
        maxSize: intensity == DemoIntensity.heavy ? 16 : 13,
        sway: 0.94,
        rotationSpeed: 0.72,
        shapeIds: const <String>[
          PetalShapeIds.wisteriaStandard,
          PetalShapeIds.wisteriaNarrow,
          PetalShapeIds.wisteriaFolded,
        ],
      );
    case DemoEffectKind.firefly:
      return FireflyEffect(
        intensity: _mapFireflyIntensity(intensity),
        speed: speed,
        opacity: 0.96 * opacity,
        particleCount: particleCount,
        minSize: 1.8,
        maxSize: intensity == DemoIntensity.heavy ? 4.2 : 3.8,
        wander: 1.0,
        blinkSpeed: 1.0,
        glowColor: const Color(0xFFF3F08A),
        coreColor: const Color(0xFFFFF9D1),
      );
  }
}

/// 前景层示例使用单独一套更克制的参数。
///
/// 前景特效的目的不是把内容盖住,而是补一点近景层次感。
/// 所以这里统一走“数量更少、体积略大、透明度更克制”的方向,
/// 避免直接照搬背景层参数后,把文字和按钮压得发脏。
AmbientEffectConfig _buildForegroundDemoEffect(
  DemoEffectKind kind,
  DemoIntensity intensity, {
  required double speed,
  required double opacity,
}) {
  switch (kind) {
    case DemoEffectKind.rain:
      return RainEffect(
        intensity: _mapRainIntensity(intensity),
        speed: speed * 1.35,
        opacity: 0.34 * opacity,
        particleCount: 14,
        color: const Color(0xFFEAF4FF),
        angle: -0.24,
        wind: 0.18,
        strokeWidth: 1.75,
      );
    case DemoEffectKind.snow:
      return SnowEffect(
        intensity: _mapSnowIntensity(intensity),
        speed: speed * 0.92,
        opacity: 0.68 * opacity,
        particleCount: 12,
        minSize: 4.0,
        maxSize: intensity == DemoIntensity.heavy ? 9.0 : 7.2,
        drift: 1.25,
        color: const Color(0xFFFFFFFF),
      );
    case DemoEffectKind.leaf:
      return LeafEffect(
        intensity: _mapLeafIntensity(intensity),
        speed: speed * 0.94,
        opacity: 0.82 * opacity,
        particleCount: 10,
        minSize: 14,
        maxSize: intensity == DemoIntensity.heavy ? 26 : 22,
        sway: 1.18,
        rotationSpeed: 1.08,
        palette: const <Color>[
          Color(0xFFE28739),
          Color(0xFFD45A1F),
          Color(0xFFF0B067),
          Color(0xFFB84720),
        ],
      );
    case DemoEffectKind.sakura:
      return SakuraEffect(
        intensity: _mapSakuraIntensity(intensity),
        speed: speed * 0.9,
        opacity: 0.84 * opacity,
        particleCount: 10,
        minSize: 10,
        maxSize: intensity == DemoIntensity.heavy ? 22 : 18,
        sway: 1.24,
        rotationSpeed: 0.96,
        palette: const <Color>[
          Color(0xFFFFD7E6),
          Color(0xFFFFC3DB),
          Color(0xFFFFE7F0),
          Color(0xFFF5AFC8),
        ],
      );
    case DemoEffectKind.wisteria:
      return PetalEffect(
        style: PetalStyle.wisteria,
        intensity: _mapPetalIntensity(intensity),
        speed: speed * 0.7,
        opacity: 0.78 * opacity,
        particleCount: 9,
        minSize: 8,
        maxSize: intensity == DemoIntensity.heavy ? 17 : 14,
        sway: 0.88,
        rotationSpeed: 0.64,
        shapeIds: const <String>[
          PetalShapeIds.wisteriaStandard,
          PetalShapeIds.wisteriaFolded,
        ],
      );
    case DemoEffectKind.firefly:
      return FireflyEffect(
        intensity: _mapFireflyIntensity(intensity),
        speed: speed * 0.92,
        opacity: 0.92 * opacity,
        particleCount: 10,
        minSize: 2.2,
        maxSize: intensity == DemoIntensity.heavy ? 4.6 : 4.0,
        wander: 0.92,
        blinkSpeed: 1.08,
        glowColor: const Color(0xFFF6F39B),
        coreColor: const Color(0xFFFFFCE0),
      );
  }
}

String _effectLabel(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => 'Rain',
    DemoEffectKind.snow => 'Snow',
    DemoEffectKind.leaf => 'Leaf',
    DemoEffectKind.sakura => 'Sakura',
    DemoEffectKind.wisteria => 'Wisteria',
    DemoEffectKind.firefly => 'Firefly',
  };
}

String _effectSummary(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain =>
      'Fast-moving streaks for stormy headers, hero sections, and immersive weather scenes.',
    DemoEffectKind.snow =>
      'Soft drifting flakes that work well for landing pages, winter cards, and festive moments.',
    DemoEffectKind.leaf =>
      'Warm falling leaves for autumn scenes, seasonal promotions, and cozy editorial layouts.',
    DemoEffectKind.sakura =>
      'Floating petals for romantic views, spring themes, and soft decorative transitions.',
    DemoEffectKind.wisteria =>
      'Layered wisteria petals with mixed narrow and folded shapes for softer, hanging floral motion.',
    DemoEffectKind.firefly =>
      'Slow wandering glow points for gardens, night scenes, and calm ambient storytelling.',
  };
}

String _effectCardHint(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => 'Linear streaks with wind control',
    DemoEffectKind.snow => 'Dots and crystal flakes with drift',
    DemoEffectKind.leaf => 'Layered leaves with sway and rotation',
    DemoEffectKind.sakura => 'Petals with gentle swing and rotation',
    DemoEffectKind.wisteria => 'Mixed petal shapes with softer swing',
    DemoEffectKind.firefly => 'Free-flight glow with soft blinking',
  };
}

String _headlineText(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => 'Rain over the bay',
    DemoEffectKind.snow => 'Snow above the ridge',
    DemoEffectKind.leaf => 'Leaves across the trail',
    DemoEffectKind.sakura => 'Petals over the garden',
    DemoEffectKind.wisteria => 'Wisteria through the courtyard',
    DemoEffectKind.firefly => 'Fireflies in the grove',
  };
}

String _detailDescription(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain =>
      'Tune density, speed, and opacity for a rain scene while comparing full-page and local-container usage.',
    DemoEffectKind.snow =>
      'Adjust the snow scene and compare how the same effect behaves behind the page and inside a compact card.',
    DemoEffectKind.leaf =>
      'Explore an autumn setup with the same effect pipeline applied to both broad layouts and local containers.',
    DemoEffectKind.sakura =>
      'Refine a petal scene and verify that the same configuration still reads well inside a compact widget.',
    DemoEffectKind.wisteria =>
      'Preview the current wisteria petal mix and compare how the same petal system behaves as both a page effect and a local card overlay.',
    DemoEffectKind.firefly =>
      'Control a natural firefly scene and compare full-page ambience with a smaller content card preview.',
  };
}

IconData _effectIcon(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => Icons.grain,
    DemoEffectKind.snow => Icons.ac_unit,
    DemoEffectKind.leaf => Icons.park,
    DemoEffectKind.sakura => Icons.local_florist,
    DemoEffectKind.wisteria => Icons.spa,
    DemoEffectKind.firefly => Icons.wb_twilight,
  };
}

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

Color _foregroundSurfaceColor(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => const Color(0xFF31485E),
    DemoEffectKind.snow => const Color(0xFFEAF3F8),
    DemoEffectKind.leaf => const Color(0xFFF8E8DA),
    DemoEffectKind.sakura => const Color(0xFFFFEEF4),
    DemoEffectKind.wisteria => const Color(0xFFF2EEFB),
    DemoEffectKind.firefly => const Color(0xFF243227),
  };
}

Color _foregroundPrimaryTextColor(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => Colors.white,
    DemoEffectKind.snow => const Color(0xFF1F2A33),
    DemoEffectKind.leaf => const Color(0xFF3D2A1E),
    DemoEffectKind.sakura => const Color(0xFF4E3340),
    DemoEffectKind.wisteria => const Color(0xFF47385F),
    DemoEffectKind.firefly => const Color(0xFFF2F6E8),
  };
}

Color _foregroundSecondaryTextColor(DemoEffectKind kind) {
  return switch (kind) {
    DemoEffectKind.rain => const Color(0xFFDCE8F2),
    DemoEffectKind.snow => const Color(0xFF5A6B78),
    DemoEffectKind.leaf => const Color(0xFF6A5042),
    DemoEffectKind.sakura => const Color(0xFF7A5E6C),
    DemoEffectKind.wisteria => const Color(0xFF75688E),
    DemoEffectKind.firefly => const Color(0xFFD5E0C7),
  };
}

LinearGradient _panelGradient(DemoEffectKind kind) {
  return switch (kind) {
    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,
    ),
    DemoEffectKind.wisteria => const LinearGradient(
      colors: <Color>[Color(0xFFE2D8F8), Color(0xFFF7F3FD)],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
    DemoEffectKind.firefly => const LinearGradient(
      colors: <Color>[Color(0xFFE6F0D7), Color(0xFFF7FAF0)],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    ),
  };
}

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,
  };
}

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Ambient Effects Demo'),
        backgroundColor: Colors.white,
        elevation: 0,
        surfaceTintColor: Colors.white,
        foregroundColor: const Color(0xFF1F2A33),
      ),
      body: SafeArea(
        top: false,
        child: SingleChildScrollView(
          padding: const EdgeInsets.fromLTRB(20, 20, 20, 24),
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 960),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    'Tap a card to open the dedicated demo page for that effect.',
                    style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                      height: 1.55,
                      color: const Color(0xFF536574),
                    ),
                  ),
                  const SizedBox(height: 20),
                  for (final DemoEffectKind kind
                      in DemoEffectKind.values) ...<Widget>[
                    _EffectShowcaseCard(effectKind: kind),
                    if (kind != DemoEffectKind.values.last)
                      const SizedBox(height: 18),
                  ],
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// 首页卡片专门用来强调“局部包裹任意 widget”这个能力。
///
/// 所以这里故意保持页面背景纯白,只让卡片自身带效果层。用户一眼就能看出
/// 插件不只适合整页背景,也能稳定放进局部组件里。
class _EffectShowcaseCard extends StatelessWidget {
  const _EffectShowcaseCard({required this.effectKind});

  final DemoEffectKind effectKind;

  @override
  Widget build(BuildContext context) {
    final effect = _buildDemoEffect(
      effectKind,
      DemoIntensity.medium,
      speed: 1.0,
      opacity: 1.0,
      particleCount: null,
    );

    return ClipRRect(
      borderRadius: BorderRadius.circular(28),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          onTap: () {
            Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (BuildContext context) =>
                    AmbientEffectDetailPage(effectKind: effectKind),
              ),
            );
          },
          child: AmbientEffectsContainer(
            effect: effect,
            enabled: true,
            performanceMode: PerformanceMode.balanced,
            renderStyle: EffectRenderStyle.layered,
            backgroundColor: _sceneColor(effectKind),
            child: Container(
              constraints: const BoxConstraints(minHeight: 208),
              padding: const EdgeInsets.all(24),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.white.withAlpha(46)),
                gradient: LinearGradient(
                  colors: <Color>[
                    Colors.white.withAlpha(28),
                    Colors.white.withAlpha(10),
                  ],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Row(
                    children: <Widget>[
                      DecoratedBox(
                        decoration: BoxDecoration(
                          color: Colors.white.withAlpha(38),
                          borderRadius: BorderRadius.circular(999),
                        ),
                        child: Padding(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 12,
                            vertical: 8,
                          ),
                          child: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: <Widget>[
                              Icon(
                                _effectIcon(effectKind),
                                size: 18,
                                color: Colors.white,
                              ),
                              const SizedBox(width: 8),
                              Text(
                                _effectLabel(effectKind),
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontWeight: FontWeight.w700,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                      const Spacer(),
                      const Icon(
                        Icons.arrow_forward_rounded,
                        color: Colors.white,
                      ),
                    ],
                  ),
                  const SizedBox(height: 18),
                  Text(
                    _headlineText(effectKind),
                    style: const TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.w700,
                      color: Colors.white,
                    ),
                  ),
                  const SizedBox(height: 10),
                  Text(
                    _effectSummary(effectKind),
                    style: const TextStyle(
                      fontSize: 15,
                      height: 1.5,
                      color: Color(0xFFE1EBF0),
                    ),
                  ),
                  const SizedBox(height: 20),
                  Wrap(
                    spacing: 12,
                    runSpacing: 12,
                    children: <Widget>[
                      const _MetricChip(
                        label: 'Usage',
                        value: 'Local container demo',
                      ),
                      _MetricChip(
                        label: 'Focus',
                        value: _effectCardHint(effectKind),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class AmbientEffectDetailPage extends StatefulWidget {
  const AmbientEffectDetailPage({super.key, required this.effectKind});

  final DemoEffectKind effectKind;

  @override
  State<AmbientEffectDetailPage> createState() =>
      _AmbientEffectDetailPageState();
}

class _AmbientEffectDetailPageState extends State<AmbientEffectDetailPage> {
  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;

  /// slider 用 0 表示“交给效果自己决定默认粒子数”,
  /// 而不是强行把 0 也当成真正的粒子数量传下去。
  ///
  /// 这样一来,示例页既能演示自动档,也能演示手动覆盖,不需要额外再做一套
  /// “是否启用 override”的开关。
  int? get _particleCountOverride {
    final value = _particleSliderValue.round();
    return value <= 0 ? null : value;
  }

  AmbientEffectConfig get _effect => _buildDemoEffect(
    widget.effectKind,
    _intensity,
    speed: _speed,
    opacity: _opacity,
    particleCount: _particleCountOverride,
  );

  AmbientEffectConfig get _foregroundEffect => _buildForegroundDemoEffect(
    widget.effectKind,
    _intensity,
    speed: _speed,
    opacity: _opacity,
  );

  @override
  Widget build(BuildContext context) {
    return AmbientEffectsContainer(
      effect: _effect,
      enabled: _enabled,
      performanceMode: _performanceMode,
      renderStyle: _renderStyle,
      backgroundColor: _sceneColor(widget.effectKind),
      child: Scaffold(
        backgroundColor: Colors.transparent,
        appBar: AppBar(
          backgroundColor: Colors.transparent,
          elevation: 0,
          surfaceTintColor: Colors.transparent,
          foregroundColor: Colors.white,
          title: Text('${_effectLabel(widget.effectKind)} Demo'),
        ),
        body: SafeArea(
          top: false,
          child: SingleChildScrollView(
            padding: const EdgeInsets.fromLTRB(20, 8, 20, 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(),
                                  ),
                                  const SizedBox(width: 20),
                                  Expanded(
                                    flex: 2,
                                    child: _buildControlPanel(),
                                  ),
                                ],
                              );
                            }

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

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

    return DecoratedBox(
      decoration: BoxDecoration(
        gradient: _panelGradient(widget.effectKind),
        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(
              _effectLabel(widget.effectKind),
              style: theme.textTheme.labelLarge?.copyWith(
                letterSpacing: 1.1,
                color: const Color(0xFF4D5F6E),
              ),
            ),
            const SizedBox(height: 10),
            Text(
              'Ambient Effects Container',
              style: theme.textTheme.headlineMedium?.copyWith(
                fontWeight: FontWeight.w700,
                color: const Color(0xFF1F2A33),
              ),
            ),
            const SizedBox(height: 12),
            Text(
              _detailDescription(widget.effectKind),
              style: theme.textTheme.bodyLarge?.copyWith(
                height: 1.5,
                color: const Color(0xFF405261),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPreviewPanel() {
    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(widget.effectKind),
                      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: _effectLabel(widget.effectKind),
                        ),
                        _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: 'Background + Foreground Demo',
          subtitle:
              'A compact card with both layers enabled so you can preview how near-field particles sit above the content.',
          child: AmbientEffectsContainer(
            effect: _effect,
            foregroundEffect: _foregroundEffect,
            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),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
        const SizedBox(height: 20),
        _PreviewCard(
          title: 'Foreground Overlay Demo',
          subtitle:
              'The content stays fully opaque while the effect renders above it and still lets all gestures pass through.',
          child: AmbientEffectsContainer(
            foregroundEffect: _foregroundEffect,
            enabled: _enabled,
            performanceMode: _performanceMode,
            renderStyle: _renderStyle,
            child: Builder(
              builder: (BuildContext context) {
                final surfaceColor = _foregroundSurfaceColor(widget.effectKind);
                final primaryTextColor = _foregroundPrimaryTextColor(
                  widget.effectKind,
                );
                final secondaryTextColor = _foregroundSecondaryTextColor(
                  widget.effectKind,
                );

                return Container(
                  constraints: const BoxConstraints(minHeight: 220),
                  padding: const EdgeInsets.all(24),
                  decoration: BoxDecoration(
                    color: surfaceColor,
                    borderRadius: BorderRadius.circular(24),
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        'Foreground overlay',
                        style: Theme.of(context).textTheme.titleLarge?.copyWith(
                          fontWeight: FontWeight.w700,
                          color: primaryTextColor,
                        ),
                      ),
                      const SizedBox(height: 10),
                      Text(
                        'This card does not rely on transparent surfaces. The overlay is painted above the widget tree, so it works for solid cards and still keeps interactions intact.',
                        style: TextStyle(
                          fontSize: 14,
                          height: 1.5,
                          color: secondaryTextColor,
                        ),
                      ),
                      const SizedBox(height: 24),
                      OverflowBar(
                        spacing: 12,
                        overflowSpacing: 12,
                        alignment: MainAxisAlignment.start,
                        children: <Widget>[
                          FilledButton.tonal(
                            onPressed: () {},
                            child: const Text('Primary action'),
                          ),
                          OutlinedButton(
                            onPressed: () {},
                            style: OutlinedButton.styleFrom(
                              foregroundColor: primaryTextColor,
                              side: BorderSide(
                                color: primaryTextColor.withAlpha(117),
                              ),
                            ),
                            child: const Text('Secondary'),
                          ),
                        ],
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ),
      ],
    );
  }

  /// 详情页不再提供“效果类型”切换。
  ///
  /// 进入某个详情页,本身就意味着用户正在观察单一效果;如果这里还保留切换,
  /// 路由层级和当前展示的内容就会互相打架,后续新增效果时也会让 demo 导航变得
  /// 越来越混乱。
  Widget _buildControlPanel() {
    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: '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;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

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
48
downloads

Documentation

API reference

Publisher

verified publisherblog.zyaire.top

Weekly Downloads

A reusable Flutter ambient background container with rain, snow, leaf, petal, and firefly 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