palette_generator_plus 1.0.0
palette_generator_plus: ^1.0.0 copied to clipboard
Extract prominent colors from images. A maintained palette_generator successor.
// Copyright 2026 Kerem Bas. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:palette_generator_plus/palette_generator_plus.dart';
void main() => runApp(const ExampleApp());
/// The demo application root.
class ExampleApp extends StatelessWidget {
/// Creates the demo application.
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'palette_generator_plus',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xff3f630c),
useMaterial3: true,
brightness: Brightness.light,
),
home: const HomePage(),
);
}
}
/// A bundled demo image.
class _Sample {
const _Sample(this.label, this.asset);
final String label;
final String asset;
}
const List<_Sample> _samples = <_Sample>[
_Sample('Landscape', 'assets/landscape.png'),
_Sample('Color blocks', 'assets/dominant.png'),
];
/// The single-screen demo.
class HomePage extends StatefulWidget {
/// Creates the demo home page.
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
_Sample _sample = _samples.first;
bool _useLegacy = false;
bool _runInIsolate = true;
PaletteGenerator? _palette;
bool _loading = false;
@override
void initState() {
super.initState();
_generate();
}
Future<void> _generate() async {
setState(() => _loading = true);
final PaletteGenerator palette = await PaletteGenerator.fromImageProvider(
AssetImage(_sample.asset),
maximumColorCount: 20,
quantizer: _useLegacy ? const LegacyQuantizer() : const CelebiQuantizer(),
runInIsolate: _runInIsolate,
);
if (!mounted) {
return;
}
setState(() {
_palette = palette;
_loading = false;
});
}
@override
Widget build(BuildContext context) {
final PaletteGenerator? palette = _palette;
return Scaffold(
appBar: AppBar(title: const Text('palette_generator_plus')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_controls(),
const SizedBox(height: 16),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
_sample.asset,
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
),
const SizedBox(height: 24),
if (_loading || palette == null)
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
))
else ...<Widget>[
_section('Generated Material 3 theme'),
_SchemePreview(palette: palette),
const SizedBox(height: 24),
_section('Named swatches'),
_NamedSwatches(palette: palette),
const SizedBox(height: 24),
_section('All colors (with population)'),
_AllSwatches(palette: palette),
],
],
),
);
}
Widget _controls() {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Wrap(
spacing: 8,
children: <Widget>[
for (final _Sample sample in _samples)
ChoiceChip(
label: Text(sample.label),
selected: _sample == sample,
onSelected: (_) {
setState(() => _sample = sample);
_generate();
},
),
],
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Use LegacyQuantizer (median cut)'),
subtitle: const Text('Off uses the default CelebiQuantizer'),
value: _useLegacy,
onChanged: (bool value) {
setState(() => _useLegacy = value);
_generate();
},
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Run in background isolate'),
subtitle: const Text('Falls back to the main thread on web'),
value: _runInIsolate,
onChanged: (bool value) {
setState(() => _runInIsolate = value);
_generate();
},
),
],
),
),
);
}
Widget _section(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
);
}
}
/// Shows the light and dark [ColorScheme]s generated from the palette.
class _SchemePreview extends StatelessWidget {
const _SchemePreview({required this.palette});
final PaletteGenerator palette;
@override
Widget build(BuildContext context) {
final ColorScheme light = palette.toColorScheme();
final ColorScheme dark = palette.toColorScheme(brightness: Brightness.dark);
return Row(
children: <Widget>[
Expanded(child: _schemeCard('Light', light)),
const SizedBox(width: 12),
Expanded(child: _schemeCard('Dark', dark)),
],
);
}
Widget _schemeCard(String label, ColorScheme scheme) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: scheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: scheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(label, style: TextStyle(color: scheme.onSurface)),
const SizedBox(height: 8),
_swatchRow('Primary', scheme.primary, scheme.onPrimary),
_swatchRow('Secondary', scheme.secondary, scheme.onSecondary),
_swatchRow('Tertiary', scheme.tertiary, scheme.onTertiary),
],
),
);
}
Widget _swatchRow(String label, Color color, Color onColor) {
return Container(
margin: const EdgeInsets.only(top: 6),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
width: double.infinity,
child: Text(label, style: TextStyle(color: onColor)),
);
}
}
/// Shows the named target swatches (dominant, vibrant, muted, etc.).
class _NamedSwatches extends StatelessWidget {
const _NamedSwatches({required this.palette});
final PaletteGenerator palette;
@override
Widget build(BuildContext context) {
final Map<String, PaletteColor?> named = <String, PaletteColor?>{
'Dominant': palette.dominantColor,
'Vibrant': palette.vibrantColor,
'Light Vibrant': palette.lightVibrantColor,
'Dark Vibrant': palette.darkVibrantColor,
'Muted': palette.mutedColor,
'Light Muted': palette.lightMutedColor,
'Dark Muted': palette.darkMutedColor,
};
return Column(
children: <Widget>[
for (final MapEntry<String, PaletteColor?> entry in named.entries)
_NamedSwatchTile(label: entry.key, swatch: entry.value),
],
);
}
}
class _NamedSwatchTile extends StatelessWidget {
const _NamedSwatchTile({required this.label, required this.swatch});
final String label;
final PaletteColor? swatch;
@override
Widget build(BuildContext context) {
final PaletteColor? value = swatch;
if (value == null) {
return ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const SizedBox(
width: 48,
height: 32,
child: Placeholder(strokeWidth: 1),
),
title: Text(label),
subtitle: const Text('not found'),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: value.color,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: <Widget>[
// accessibleOnColor() picks black or white for readable text.
Text(label, style: TextStyle(color: value.accessibleOnColor())),
const Spacer(),
Text(
'#${value.color.toARGB32().toRadixString(16).substring(2)}',
style: TextStyle(
color: value.accessibleOnColor(),
fontFeatures: const <FontFeature>[FontFeature.tabularFigures()],
),
),
],
),
);
}
}
/// Shows every extracted color with its population.
class _AllSwatches extends StatelessWidget {
const _AllSwatches({required this.palette});
final PaletteGenerator palette;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
for (final PaletteColor swatch in palette.paletteColors)
Container(
width: 96,
height: 64,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: swatch.color,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.bottomLeft,
child: Text(
'${swatch.population}',
style: TextStyle(
color: swatch.accessibleOnColor(),
fontWeight: FontWeight.bold,
),
),
),
],
);
}
}