Skip to main content

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

Pure Flutter animated GrokBot avatar with expressions, shapes, states, gaze, head turn, blink, and CustomPaint rendering.

example/lib/main.dart

import 'dart:math' as math;

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

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

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

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'GrokBot Widget',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorSchemeSeed: const Color(0xff5b7fe5),
          useMaterial3: true,
        ),
        darkTheme: ThemeData(
          brightness: Brightness.dark,
          colorSchemeSeed: const Color(0xff6689ea),
          useMaterial3: true,
        ),
        home: const GrokBotDemoPage(),
      );
}

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

  @override
  State<GrokBotDemoPage> createState() => _GrokBotDemoPageState();
}

class _GrokBotDemoPageState extends State<GrokBotDemoPage> {
  final _controller = GrokBotController();
  GrokBotState _state = GrokBotState.idle;
  GrokBotShape _shape = GrokBotShape.blob;
  Offset _gaze = Offset.zero;
  double _turn = 0;
  bool _autoExpression = false;

  @override
  Widget build(BuildContext context) {
    final dark = Theme.of(context).brightness == Brightness.dark;
    return Scaffold(
      appBar: AppBar(title: const Text('GrokBot Widget')),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
          children: [
            Center(
              child: GrokBot(
                controller: _controller,
                size: 260,
                state: _state,
                shape: _shape,
                gaze: _gaze,
                turn: _turn,
                autoExpression: _autoExpression,
                theme: dark ? GrokBotThemeData.dark : const GrokBotThemeData(),
                semanticLabel: 'Animated GrokBot preview',
              ),
            ),
            const SizedBox(height: 20),
            Wrap(
              alignment: WrapAlignment.center,
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton.tonal(
                  onPressed: _controller.blink,
                  child: const Text('Blink'),
                ),
                FilledButton.tonal(
                  onPressed: _controller.spin,
                  child: const Text('Spin'),
                ),
                OutlinedButton(
                  onPressed: _controller.reset,
                  child: const Text('Reset'),
                ),
              ],
            ),
            const SizedBox(height: 24),
            _Selector<GrokBotState>(
              label: 'State',
              value: _state,
              values: GrokBotState.values,
              itemLabel: (value) => value.sourceName,
              onChanged: (value) => setState(() => _state = value),
            ),
            const SizedBox(height: 12),
            _Selector<GrokBotShape>(
              label: 'Shape',
              value: _shape,
              values: GrokBotShape.values,
              itemLabel: (value) => value.name,
              onChanged: (value) => setState(() => _shape = value),
            ),
            SwitchListTile(
              contentPadding: EdgeInsets.zero,
              title: const Text('Automatic expressions'),
              value: _autoExpression,
              onChanged: (value) => setState(() => _autoExpression = value),
            ),
            _LabeledSlider(
              label: 'Gaze X',
              value: _gaze.dx,
              minimum: -1,
              maximum: 1,
              onChanged: (value) =>
                  setState(() => _gaze = Offset(value, _gaze.dy)),
            ),
            _LabeledSlider(
              label: 'Gaze Y',
              value: _gaze.dy,
              minimum: -1,
              maximum: 1,
              onChanged: (value) =>
                  setState(() => _gaze = Offset(_gaze.dx, value)),
            ),
            _LabeledSlider(
              label: 'Turn',
              value: _turn,
              minimum: -math.pi * 5 / 9,
              maximum: math.pi * 5 / 9,
              valueLabel: '${(_turn * 180 / math.pi).round()}°',
              onChanged: (value) => setState(() => _turn = value),
            ),
          ],
        ),
      ),
    );
  }
}

class _Selector<T> extends StatelessWidget {
  const _Selector({
    required this.label,
    required this.value,
    required this.values,
    required this.itemLabel,
    required this.onChanged,
  });

  final String label;
  final T value;
  final List<T> values;
  final String Function(T) itemLabel;
  final ValueChanged<T> onChanged;

  @override
  Widget build(BuildContext context) => Row(
        children: [
          SizedBox(width: 72, child: Text(label)),
          Expanded(
            child: DropdownButton<T>(
              value: value,
              isExpanded: true,
              items: [
                for (final item in values)
                  DropdownMenuItem(value: item, child: Text(itemLabel(item))),
              ],
              onChanged: (next) {
                if (next != null) onChanged(next);
              },
            ),
          ),
        ],
      );
}

class _LabeledSlider extends StatelessWidget {
  const _LabeledSlider({
    required this.label,
    required this.value,
    required this.minimum,
    required this.maximum,
    required this.onChanged,
    this.valueLabel,
  });

  final String label;
  final double value;
  final double minimum;
  final double maximum;
  final ValueChanged<double> onChanged;
  final String? valueLabel;

  @override
  Widget build(BuildContext context) => Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('$label  ${valueLabel ?? value.toStringAsFixed(2)}'),
          Slider(
              value: value, min: minimum, max: maximum, onChanged: onChanged),
        ],
      );
}
0
likes
155
points
70
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Pure Flutter animated GrokBot avatar with expressions, shapes, states, gaze, head turn, blink, and CustomPaint rendering.

Repository (GitHub)
View/report issues

Topics

#flutter #widget #animation #avatar #ui

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on grokbot_widget