grokbot_widget 0.1.0
grokbot_widget: ^0.1.0 copied to clipboard
Pure Flutter animated GrokBot avatar with expressions, shapes, states, gaze, head turn, blink, and CustomPaint rendering.
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),
],
);
}