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

Assistant-style wave animations for Flutter: Gemini ribbons, Claude surfaces, Perplexity bars, a ChatGPT orb, plus ten frequency visualisers and a wave-filled play button. Controller driven and pure C [...]

example/lib/main.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'pages/chatgpt_page.dart';
import 'pages/claude_page.dart';
import 'pages/gemini_page.dart';
import 'pages/perplexity_page.dart';
import 'pages/visualisers_page.dart';
import 'widgets/wave_stage.dart';

void main() => runApp(const WaveGalleryApp());

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

  @override
  State<WaveGalleryApp> createState() => _WaveGalleryAppState();
}

class _WaveGalleryAppState extends State<WaveGalleryApp> {
  ThemeMode _themeMode = ThemeMode.dark;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ai_wave_animations',
      debugShowCheckedModeBanner: false,
      themeMode: _themeMode,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF5489D6),
        brightness: Brightness.light,
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: const Color(0xFF5489D6),
        brightness: Brightness.dark,
      ),
      home: GalleryPage(
        themeMode: _themeMode,
        onThemeModeChanged: (mode) => setState(() => _themeMode = mode),
      ),
    );
  }
}

class GalleryPage extends StatefulWidget {
  const GalleryPage({
    super.key,
    required this.themeMode,
    required this.onThemeModeChanged,
  });

  final ThemeMode themeMode;
  final ValueChanged<ThemeMode> onThemeModeChanged;

  @override
  State<GalleryPage> createState() => _GalleryPageState();
}

class _GalleryPageState extends State<GalleryPage> {
  final WaveController _controller = WaveController(
    activity: WaveActivity.speaking,
  );
  final math.Random _random = math.Random();

  int _index = 0;
  bool _simulateMic = false;
  Timer? _micTimer;

  @override
  void dispose() {
    _micTimer?.cancel();
    _controller.dispose();
    super.dispose();
  }

  void _toggleMicSimulation(bool value) {
    setState(() => _simulateMic = value);
    _micTimer?.cancel();
    if (!value) {
      _controller.level = 0;
      return;
    }
    // Stand-in for a real microphone RMS stream.
    _micTimer = Timer.periodic(const Duration(milliseconds: 90), (_) {
      final base =
          0.5 + 0.5 * math.sin(DateTime.now().millisecondsSinceEpoch / 700);
      _controller.level = (base * 0.7 + _random.nextDouble() * 0.3)
          .clamp(0.0, 1.0)
          .toDouble();
    });
  }

  @override
  Widget build(BuildContext context) {
    final pages = <Widget>[
      OverviewPage(controller: _controller),
      GeminiPage(controller: _controller),
      ClaudePage(controller: _controller),
      PerplexityPage(controller: _controller),
      ChatGptPage(controller: _controller),
      VisualisersPage(controller: _controller),
    ];
    const destinations = <NavigationDestination>[
      NavigationDestination(icon: Icon(Icons.grid_view), label: 'All'),
      NavigationDestination(icon: Icon(Icons.auto_awesome), label: 'Gemini'),
      NavigationDestination(icon: Icon(Icons.waves), label: 'Claude'),
      NavigationDestination(icon: Icon(Icons.equalizer), label: 'Perplexity'),
      NavigationDestination(icon: Icon(Icons.blur_circular), label: 'ChatGPT'),
      NavigationDestination(icon: Icon(Icons.graphic_eq), label: 'Visualisers'),
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('ai_wave_animations'),
        actions: [
          IconButton(
            tooltip: 'Toggle theme',
            icon: Icon(
              widget.themeMode == ThemeMode.dark
                  ? Icons.light_mode
                  : Icons.dark_mode,
            ),
            onPressed: () => widget.onThemeModeChanged(
              widget.themeMode == ThemeMode.dark
                  ? ThemeMode.light
                  : ThemeMode.dark,
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          _DriverBar(
            controller: _controller,
            simulateMic: _simulateMic,
            onSimulateMicChanged: _toggleMicSimulation,
          ),
          const Divider(height: 1),
          Expanded(
            child: IndexedStack(index: _index, children: pages),
          ),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _index,
        destinations: destinations,
        onDestinationSelected: (i) => setState(() => _index = i),
      ),
    );
  }
}

/// The shared controller strip: activity, live level, play / pause.
class _DriverBar extends StatelessWidget {
  const _DriverBar({
    required this.controller,
    required this.simulateMic,
    required this.onSimulateMicChanged,
  });

  final WaveController controller;
  final bool simulateMic;
  final ValueChanged<bool> onSimulateMicChanged;

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: controller,
      builder: (context, _) {
        return Padding(
          padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
          child: Wrap(
            spacing: 12,
            runSpacing: 8,
            crossAxisAlignment: WrapCrossAlignment.center,
            children: [
              SegmentedButton<WaveActivity>(
                showSelectedIcon: false,
                segments: [
                  for (final activity in WaveActivity.values)
                    ButtonSegment(value: activity, label: Text(activity.name)),
                ],
                selected: {controller.activity},
                onSelectionChanged: (set) => controller.activity = set.first,
              ),
              FilledButton.tonalIcon(
                onPressed: controller.toggle,
                icon: Icon(
                  controller.isAnimating ? Icons.pause : Icons.play_arrow,
                ),
                label: Text(controller.isAnimating ? 'Pause' : 'Play'),
              ),
              SizedBox(
                width: 260,
                child: Row(
                  children: [
                    const Text('level'),
                    Expanded(
                      child: Slider(
                        value: controller.level,
                        onChanged: simulateMic
                            ? null
                            : (v) => controller.level = v,
                      ),
                    ),
                    Text(controller.level.toStringAsFixed(2)),
                  ],
                ),
              ),
              Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Text('simulate mic'),
                  Switch(value: simulateMic, onChanged: onSimulateMicChanged),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

/// All four styles side by side, driven by the same controller.
class OverviewPage extends StatelessWidget {
  const OverviewPage({super.key, required this.controller});

  final WaveController controller;

  @override
  Widget build(BuildContext context) {
    final cards = <_OverviewCard>[
      _OverviewCard(
        title: 'GeminiWave',
        subtitle: 'Layered multi-hue ribbons',
        background: const Color(0xFF0B0F1A),
        child: GeminiWave(
          height: 120,
          blendMode: BlendMode.plus,
          controller: controller,
        ),
      ),
      _OverviewCard(
        title: 'ClaudeWave',
        subtitle: 'Warm overlapping surfaces',
        background: WavePalette.claudePaper,
        child: ClaudeWave(height: 180, controller: controller),
      ),
      _OverviewCard(
        title: 'PerplexityWave',
        subtitle: 'Travelling equaliser bars',
        background: WavePalette.perplexityInk,
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: PerplexityWave(height: 96, controller: controller),
        ),
      ),
      _OverviewCard(
        title: 'ChatGptWave',
        subtitle: 'Breathing rippled orb',
        background: WavePalette.chatGptInk,
        child: ChatGptWave(
          width: 180,
          height: 180,
          ringWidth: 1.5,
          controller: controller,
        ),
      ),
    ];

    return LayoutBuilder(
      builder: (context, constraints) {
        final columns = constraints.maxWidth > 1000 ? 2 : 1;
        return GridView.count(
          padding: const EdgeInsets.all(16),
          crossAxisCount: columns,
          crossAxisSpacing: 16,
          mainAxisSpacing: 16,
          childAspectRatio: columns == 1 ? 1.35 : 1.5,
          children: cards,
        );
      },
    );
  }
}

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

  final String title;
  final String subtitle;
  final Color background;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(title, style: theme.textTheme.titleMedium),
        Text(subtitle, style: theme.textTheme.bodySmall),
        const SizedBox(height: 8),
        Expanded(
          child: WaveStage(
            height: double.infinity,
            background: background,
            child: child,
          ),
        ),
      ],
    );
  }
}
1
likes
150
points
34
downloads

Documentation

API reference

Publisher

verified publisherineelakandan.in

Weekly Downloads

Assistant-style wave animations for Flutter: Gemini ribbons, Claude surfaces, Perplexity bars, a ChatGPT orb, plus ten frequency visualisers and a wave-filled play button. Controller driven and pure CustomPaint, so it runs on every platform.

Homepage
Repository (GitHub)
View/report issues

Topics

#animation #waveform #voice #ui #custom-paint

License

MIT (license)

Dependencies

flutter

More

Packages that depend on ai_wave_animations