beanheads 0.1.1 copy "beanheads: ^0.1.1" to clipboard
beanheads: ^0.1.1 copied to clipboard

A customizable cartoon avatar generator that produces SVG strings. Dart port of the BeanHeads library.

example/lib/main.dart

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BeanHeads Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF85C5E5),
        useMaterial3: true,
        brightness: Brightness.light,
      ),
      home: const AvatarCustomizerPage(),
    );
  }
}

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

  @override
  State<AvatarCustomizerPage> createState() => _AvatarCustomizerPageState();
}

class _AvatarCustomizerPageState extends State<AvatarCustomizerPage> {
  BeanheadConfig _config = BeanheadConfig.random();

  void _randomize() {
    setState(() {
      _config = BeanheadConfig.random();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('BeanHeads Demo'),
        centerTitle: true,
        actions: [
          FilledButton.icon(
            onPressed: _randomize,
            icon: const Icon(Icons.shuffle),
            label: const Text('Randomize'),
          ),
          const SizedBox(width: 12),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isWide = constraints.maxWidth >= 720;
          if (isWide) {
            return Row(
              children: [
                Expanded(
                  flex: 2,
                  child: _AvatarPreview(config: _config),
                ),
                const VerticalDivider(width: 1),
                Expanded(
                  flex: 3,
                  child: _ControlsPanel(
                    config: _config,
                    onConfigChanged: (c) => setState(() => _config = c),
                  ),
                ),
              ],
            );
          }
          return Column(
            children: [
              SizedBox(
                height: 280,
                child: _AvatarPreview(config: _config),
              ),
              const Divider(height: 1),
              Expanded(
                child: _ControlsPanel(
                  config: _config,
                  onConfigChanged: (c) => setState(() => _config = c),
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Avatar preview
// ---------------------------------------------------------------------------

class _AvatarPreview extends StatelessWidget {
  const _AvatarPreview({required this.config});

  final BeanheadConfig config;

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    return Container(
      color: colorScheme.surfaceContainerLowest,
      alignment: Alignment.center,
      padding: const EdgeInsets.all(24),
      child: Beanhead(
        config: config,
        width: 300,
        height: 300,
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Preview helpers
// ---------------------------------------------------------------------------

/// A base config used for generating preview thumbnails.
/// Kept simple (no hat, no accessory, no facial hair) so that
/// the changed property is clearly visible.
const _previewBase = BeanheadConfig(
  skinTone: SkinTone.light,
  eyes: Eyes.normal,
  eyebrows: Eyebrows.raised,
  mouth: Mouth.grin,
  hair: Hair.none,
  facialHair: FacialHair.none,
  clothing: Clothing.shirt,
  accessory: Accessory.none,
  graphic: ClothingGraphic.none,
  hat: Hat.none,
  body: Body.chest,
  hairColor: HairColor.brown,
  clothingColor: ClothingColor.white,
  lipColor: LipColor.red,
  hatColor: HatColor.white,
  faceMaskColor: FaceMaskColor.white,
  mask: true,
  faceMask: false,
  lashes: false,
);

/// Small avatar thumbnail widget used inside dropdown items.
class _AvatarThumbnail extends StatelessWidget {
  const _AvatarThumbnail({required this.config});

  final BeanheadConfig config;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 28,
      height: 28,
      child: Beanhead(config: config),
    );
  }
}

/// Color swatch circle widget used for color-type dropdown items.
class _ColorSwatch extends StatelessWidget {
  const _ColorSwatch({required this.hexColor});

  final String hexColor;

  @override
  Widget build(BuildContext context) {
    // Parse hex color string like '#fdd2b2' or 'fdd2b2'.
    final hex = hexColor.replaceFirst('#', '');
    final colorValue = int.parse(hex, radix: 16) | 0xFF000000;
    return SizedBox(
      width: 28,
      height: 28,
      child: Center(
        child: Container(
          width: 20,
          height: 20,
          decoration: BoxDecoration(
            color: Color(colorValue),
            shape: BoxShape.circle,
            border: Border.all(color: Colors.grey.shade400, width: 0.5),
          ),
        ),
      ),
    );
  }
}

/// Returns a preview widget builder for component-type enums.
/// Each option shows a small avatar with that specific property applied.
Widget Function(T) _avatarPreviewBuilder<T extends Enum>(
  BeanheadConfig Function(T value) configBuilder,
) {
  return (T value) => _AvatarThumbnail(config: configBuilder(value));
}

/// Returns a preview widget builder for color-type enums.
Widget Function(T) _colorSwatchBuilder<T extends Enum>(
  String Function(T value) colorResolver,
) {
  return (T value) => _ColorSwatch(hexColor: colorResolver(value));
}

// ---------------------------------------------------------------------------
// Controls panel
// ---------------------------------------------------------------------------

class _ControlsPanel extends StatelessWidget {
  const _ControlsPanel({
    required this.config,
    required this.onConfigChanged,
  });

  final BeanheadConfig config;
  final ValueChanged<BeanheadConfig> onConfigChanged;

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      children: [
        const _SectionHeader('Appearance'),
        _EnumDropdown<SkinTone>(
          label: 'Skin Tone',
          value: config.skinTone,
          values: SkinTone.values,
          onChanged: (v) => onConfigChanged(config.copyWith(skinTone: v)),
          previewBuilder: _colorSwatchBuilder<SkinTone>(
            (v) => defaultTheme.skinColors[v]!.base,
          ),
        ),
        _EnumDropdown<Body>(
          label: 'Body',
          value: config.body,
          values: Body.values,
          onChanged: (v) => onConfigChanged(config.copyWith(body: v)),
          previewBuilder: _avatarPreviewBuilder<Body>(
            (v) => _previewBase.copyWith(body: v),
          ),
        ),
        _BoolSwitch(
          label: 'Lashes',
          value: config.lashes,
          onChanged: (v) => onConfigChanged(config.copyWith(lashes: v)),
        ),
        const SizedBox(height: 4),
        const _SectionHeader('Face'),
        _EnumDropdown<Eyes>(
          label: 'Eyes',
          value: config.eyes,
          values: Eyes.values,
          onChanged: (v) => onConfigChanged(config.copyWith(eyes: v)),
          previewBuilder: _avatarPreviewBuilder<Eyes>(
            (v) => _previewBase.copyWith(eyes: v),
          ),
        ),
        _EnumDropdown<Eyebrows>(
          label: 'Eyebrows',
          value: config.eyebrows,
          values: Eyebrows.values,
          onChanged: (v) => onConfigChanged(config.copyWith(eyebrows: v)),
          previewBuilder: _avatarPreviewBuilder<Eyebrows>(
            (v) => _previewBase.copyWith(eyebrows: v),
          ),
        ),
        _EnumDropdown<Mouth>(
          label: 'Mouth',
          value: config.mouth,
          values: Mouth.values,
          onChanged: (v) => onConfigChanged(config.copyWith(mouth: v)),
          previewBuilder: _avatarPreviewBuilder<Mouth>(
            (v) => _previewBase.copyWith(mouth: v),
          ),
        ),
        _EnumDropdown<FacialHair>(
          label: 'Facial Hair',
          value: config.facialHair,
          values: FacialHair.values,
          onChanged: (v) => onConfigChanged(config.copyWith(facialHair: v)),
          previewBuilder: _avatarPreviewBuilder<FacialHair>(
            (v) => _previewBase.copyWith(facialHair: v),
          ),
        ),
        _EnumDropdown<LipColor>(
          label: 'Lip Color',
          value: config.lipColor,
          values: LipColor.values,
          onChanged: (v) => onConfigChanged(config.copyWith(lipColor: v)),
          previewBuilder: _colorSwatchBuilder<LipColor>(
            (v) => defaultTheme.lipColors[v]!.base,
          ),
        ),
        const SizedBox(height: 4),
        const _SectionHeader('Hair'),
        _EnumDropdown<Hair>(
          label: 'Hair Style',
          value: config.hair,
          values: Hair.values,
          onChanged: (v) => onConfigChanged(config.copyWith(hair: v)),
          previewBuilder: _avatarPreviewBuilder<Hair>(
            (v) => _previewBase.copyWith(hair: v),
          ),
        ),
        _EnumDropdown<HairColor>(
          label: 'Hair Color',
          value: config.hairColor,
          values: HairColor.values,
          onChanged: (v) => onConfigChanged(config.copyWith(hairColor: v)),
          previewBuilder: _colorSwatchBuilder<HairColor>(
            (v) => defaultTheme.hairColors[v]!.base,
          ),
        ),
        const SizedBox(height: 4),
        const _SectionHeader('Clothing'),
        _EnumDropdown<Clothing>(
          label: 'Clothing',
          value: config.clothing,
          values: Clothing.values,
          onChanged: (v) => onConfigChanged(config.copyWith(clothing: v)),
          previewBuilder: _avatarPreviewBuilder<Clothing>(
            (v) => _previewBase.copyWith(clothing: v),
          ),
        ),
        _EnumDropdown<ClothingColor>(
          label: 'Clothing Color',
          value: config.clothingColor,
          values: ClothingColor.values,
          onChanged: (v) => onConfigChanged(config.copyWith(clothingColor: v)),
          previewBuilder: _colorSwatchBuilder<ClothingColor>(
            (v) => defaultTheme.clothingColors[v]!.base,
          ),
        ),
        _EnumDropdown<ClothingGraphic>(
          label: 'Graphic',
          value: config.graphic,
          values: ClothingGraphic.values,
          onChanged: (v) => onConfigChanged(config.copyWith(graphic: v)),
          previewBuilder: _avatarPreviewBuilder<ClothingGraphic>(
            (v) => _previewBase.copyWith(graphic: v, clothing: Clothing.shirt),
          ),
        ),
        const SizedBox(height: 4),
        const _SectionHeader('Accessories'),
        _EnumDropdown<Accessory>(
          label: 'Accessory',
          value: config.accessory,
          values: Accessory.values,
          onChanged: (v) => onConfigChanged(config.copyWith(accessory: v)),
          previewBuilder: _avatarPreviewBuilder<Accessory>(
            (v) => _previewBase.copyWith(accessory: v),
          ),
        ),
        _EnumDropdown<Hat>(
          label: 'Hat',
          value: config.hat,
          values: Hat.values,
          onChanged: (v) => onConfigChanged(config.copyWith(hat: v)),
          previewBuilder: _avatarPreviewBuilder<Hat>(
            (v) => _previewBase.copyWith(hat: v),
          ),
        ),
        _EnumDropdown<HatColor>(
          label: 'Hat Color',
          value: config.hatColor,
          values: HatColor.values,
          onChanged: (v) => onConfigChanged(config.copyWith(hatColor: v)),
          previewBuilder: _colorSwatchBuilder<HatColor>(
            (v) => defaultTheme.clothingColors[ClothingColor.values
                    .firstWhere((c) => c.name == v.name)]!
                .base,
          ),
        ),
        const SizedBox(height: 4),
        const _SectionHeader('Other'),
        _BoolSwitch(
          label: 'Mask (circular clip)',
          value: config.mask,
          onChanged: (v) => onConfigChanged(config.copyWith(mask: v)),
        ),
        _BoolSwitch(
          label: 'Face Mask',
          value: config.faceMask,
          onChanged: (v) => onConfigChanged(config.copyWith(faceMask: v)),
        ),
        _EnumDropdown<FaceMaskColor>(
          label: 'Face Mask Color',
          value: config.faceMaskColor,
          values: FaceMaskColor.values,
          onChanged: (v) => onConfigChanged(config.copyWith(faceMaskColor: v)),
          previewBuilder: _colorSwatchBuilder<FaceMaskColor>(
            (v) => defaultTheme.clothingColors[ClothingColor.values
                    .firstWhere((c) => c.name == v.name)]!
                .base,
          ),
        ),
        const SizedBox(height: 16),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Reusable control widgets
// ---------------------------------------------------------------------------

class _SectionHeader extends StatelessWidget {
  const _SectionHeader(this.title);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 4, bottom: 2),
      child: Text(
        title,
        style: Theme.of(context).textTheme.titleSmall?.copyWith(
              fontWeight: FontWeight.bold,
              color: Theme.of(context).colorScheme.primary,
            ),
      ),
    );
  }
}

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

  final String label;
  final T value;
  final List<T> values;
  final ValueChanged<T> onChanged;

  /// Optional builder that returns a preview widget for each enum value.
  final Widget Function(T value)? previewBuilder;

  String _formatName(String name) {
    // Convert camelCase to Title Case with spaces.
    final buffer = StringBuffer();
    for (var i = 0; i < name.length; i++) {
      final ch = name[i];
      if (i > 0 && ch == ch.toUpperCase() && ch != ch.toLowerCase()) {
        buffer.write(' ');
      }
      buffer.write(i == 0 ? ch.toUpperCase() : ch);
    }
    return buffer.toString();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
            width: 120,
            child: Text(label, style: Theme.of(context).textTheme.bodySmall),
          ),
          Expanded(
            child: DropdownButtonFormField<T>(
              initialValue: value,
              isExpanded: true,
              isDense: true,
              decoration: InputDecoration(
                contentPadding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(4),
                ),
              ),
              items: values
                  .map(
                    (v) => DropdownMenuItem<T>(
                      value: v,
                      child: Row(
                        children: [
                          if (previewBuilder != null) ...[
                            previewBuilder!(v),
                            const SizedBox(width: 8),
                          ],
                          Expanded(
                            child: Text(
                              _formatName(v.name),
                              style: Theme.of(context).textTheme.bodySmall,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ],
                      ),
                    ),
                  )
                  .toList(),
              onChanged: (v) {
                if (v != null) onChanged(v);
              },
            ),
          ),
        ],
      ),
    );
  }
}

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

  final String label;
  final bool value;
  final ValueChanged<bool> onChanged;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
            width: 120,
            child: Text(label, style: Theme.of(context).textTheme.bodySmall),
          ),
          SizedBox(
            height: 28,
            child: FittedBox(
              child: Switch(
                value: value,
                onChanged: onChanged,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
1
likes
160
points
66
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A customizable cartoon avatar generator that produces SVG strings. Dart port of the BeanHeads library.

Repository (GitHub)
View/report issues

Topics

#avatar #svg #cartoon #generator

License

MIT (license)

Dependencies

flutter, flutter_svg

More

Packages that depend on beanheads