From 3952d30f4a4e9878951fa8109a7c44e95b8f9a17 Mon Sep 17 00:00:00 2001 From: Jong Hyun Kim Date: Fri, 6 Dec 2024 21:14:03 +0900 Subject: [PATCH 1/3] feat: Add ColorPicker component - Add core ColorPicker widget implementation - Implement ColorSliders and spectrum components - Add FluentUI theme integration - Add documentation and basic tests - Fix layout and label positioning issues --- CHANGELOG.md | 36 + example/lib/main.dart | 15 + example/lib/routes/forms.dart | 1 + example/lib/screens/forms/color_picker.dart | 222 +++ lib/fluent_ui.dart | 1 + .../form/color_picker/color_names.dart | 1575 +++++++++++++++++ .../form/color_picker/color_picker.dart | 1143 ++++++++++++ .../form/color_picker/color_spectrum.dart | 760 ++++++++ .../form/color_picker/color_state.dart | 520 ++++++ test/color_picker_test.dart | 147 ++ 10 files changed, 4420 insertions(+) create mode 100644 example/lib/screens/forms/color_picker.dart create mode 100644 lib/src/controls/form/color_picker/color_names.dart create mode 100644 lib/src/controls/form/color_picker/color_picker.dart create mode 100644 lib/src/controls/form/color_picker/color_spectrum.dart create mode 100644 lib/src/controls/form/color_picker/color_state.dart create mode 100644 test/color_picker_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index d89a6353c..b1106316f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ ## [next] +- feat: Add ColorPicker component following WinUI 3 specifications ([#1001](https://github.com/bdlukaa/fluent_ui/issues/1001) +- fix: Add missing properties (`closeIconSize`, `closeButtonStyle`) in `debugFillProperties` and `InfoBarThemeData.merge` ([#1128](https://github.com/bdlukaa/fluent_ui/issues/1128) +- feat: Add `TabView.reservedStripWidth`, which adds a minimum empty area between the tabs and the tab view footer ([#1106](https://github.com/bdlukaa/fluent_ui/issues/1106))] +- fix: Correctly unfocus `NumberBox` when user taps outside ([#1135](https://github.com/bdlukaa/fluent_ui/issues/1135)) +- fix: Do try to scroll Date and Time at build time ([#1117](https://github.com/bdlukaa/fluent_ui/issues/1117)) +- feat: Use a `Decoration` instead of `Color` in `NavigationAppBar` ([#1118](https://github.com/bdlukaa/fluent_ui/issues/1118)) +- feat: Add `EditableComboBox.inputFormatters` ([#1041](https://github.com/bdlukaa/fluent_ui/issues/1041)) +- **BREAKING** feat: `TextBox.decoration` and `TextBox.foregroundDecoration` are now of type `WidgetStateProperty` ([#987](https://github.com/bdlukaa/fluent_ui/pull/987)) + + Before: + ```dart + TextBox( + decoration: BoxDecoration( + color: Colors.red, + ), + foregroundDecoration: BoxDecoration( + color: Colors.blue, + ), + ), + ``` + + After: + ```dart + TextBox( + decoration: WidgetStateProperty.all(BoxDecoration( + color: Colors.red, + )), + foregroundDecoration: WidgetStateProperty.all(BoxDecoration( + color: Colors.blue, + )), + ), + ``` +- feat: Add `TabView.gestures`, which allows the manipulation of the tab gestures ([#1138](https://github.com/bdlukaa/fluent_ui/issues/1138)) +- feat: Add `DropDownButton.style` ([#1139](https://github.com/bdlukaa/fluent_ui/issues/1139)) +- feat: Possibility to open date and time pickers programatically ([#1142](https://github.com/bdlukaa/fluent_ui/issues/1142)) +- fix: `TimePicker` hour offset - fix: Add missing properties (`closeIconSize`, `closeButtonStyle`) in `debugFillProperties` and `InfoBarThemeData.merge` ([#1128](https://github.com/bdlukaa/fluent_ui/issues/1128) - feat: Add `TabView.reservedStripWidth`, which adds a minimum empty area between the tabs and the tab view footer ([#1106](https://github.com/bdlukaa/fluent_ui/issues/1106))] - fix: Correctly unfocus `NumberBox` when user taps outside ([#1135](https://github.com/bdlukaa/fluent_ui/issues/1135)) diff --git a/example/lib/main.dart b/example/lib/main.dart index 39f4673f8..7c918658c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -226,6 +226,12 @@ class _MyHomePageState extends State with WindowListener { title: const Text('DatePicker'), body: const SizedBox.shrink(), ), + PaneItem( + key: const ValueKey('/forms/color_picker'), + icon: const Icon(FluentIcons.color), + title: const Text('ColorPicker'), + body: const SizedBox.shrink(), + ), PaneItemHeader(header: const Text('Navigation')), PaneItem( key: const ValueKey('/navigation/navigation_view'), @@ -818,6 +824,15 @@ final router = GoRouter(navigatorKey: rootNavigatorKey, routes: [ ), ), + /// ColorPicker + GoRoute( + path: '/forms/color_picker', + builder: (context, state) => DeferredWidget( + forms.loadLibrary, + () => forms.ColorPickerPage(), + ), + ), + /// /// Navigation /// NavigationView GoRoute( diff --git a/example/lib/routes/forms.dart b/example/lib/routes/forms.dart index efce0cd80..a420eaff0 100644 --- a/example/lib/routes/forms.dart +++ b/example/lib/routes/forms.dart @@ -5,3 +5,4 @@ export '../screens/forms/password_box.dart'; export '../screens/forms/date_picker.dart'; export '../screens/forms/text_box.dart'; export '../screens/forms/time_picker.dart'; +export '../screens/forms/color_picker.dart'; diff --git a/example/lib/screens/forms/color_picker.dart b/example/lib/screens/forms/color_picker.dart new file mode 100644 index 000000000..da7557bb8 --- /dev/null +++ b/example/lib/screens/forms/color_picker.dart @@ -0,0 +1,222 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class ColorPickerPage extends StatefulWidget { + const ColorPickerPage({super.key}); + + @override + State createState() => _ColorPickerPageState(); +} + +class _ColorPickerPageState extends State with PageMixin { + Color _selectedColor = Colors.blue; + bool _isMoreButtonVisible = false; + bool _isColorSliderVisible = true; + bool _isColorChannelTextInputVisible = true; + bool _isHexInputVisible = true; + bool _isAlphaEnabled = false; + bool _isAlphaSliderVisible = false; + bool _isAlphaTextInputVisible = false; + bool _isColorPreviewVisible = true; + ColorSpectrumShape _spectrumShape = ColorSpectrumShape.box; + Axis _orientation = Axis.vertical; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('ColorPicker'), + commandBar: Button( + onPressed: () => setState(() { + _selectedColor = Colors.red; + _isMoreButtonVisible = false; + _isColorSliderVisible = true; + _isColorChannelTextInputVisible = true; + _isHexInputVisible = true; + _isAlphaEnabled = false; + _isAlphaSliderVisible = false; + _isAlphaTextInputVisible = false; + _isColorPreviewVisible = true; + _spectrumShape = ColorSpectrumShape.box; + _orientation = Axis.vertical; + }), + child: const Text('Reset'), + ), + ), + children: [ + const Text( + 'A ColorPicker control lets users select a color using a color spectrum, ' + 'sliders, and text input. It includes RGB (Red, Green, Blue) and HSV ' + '(Hue, Saturation, Value) color representations.\n\n' + 'The ColorPicker includes two spectrum shapes (box and ring) and several ' + 'optional components that can be shown or hidden.', + ), + const SizedBox(height: 20), + // Options + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Spectrum Shape:', + style: FluentTheme.of(context).typography.bodyStrong), + const SizedBox(height: 8), + Row(children: [ + RadioButton( + checked: _spectrumShape == ColorSpectrumShape.box, + onChanged: (v) { + if (v) { + setState(() => _spectrumShape = ColorSpectrumShape.box); + } + }, + content: const Text('Box'), + ), + const SizedBox(width: 20), + RadioButton( + checked: _spectrumShape == ColorSpectrumShape.ring, + onChanged: (v) { + if (v) { + setState(() => _spectrumShape = ColorSpectrumShape.ring); + } + }, + content: const Text('Ring'), + ), + ]), + const SizedBox(height: 20), + Text('Layout:', + style: FluentTheme.of(context).typography.bodyStrong), + const SizedBox(height: 8), + Row(children: [ + RadioButton( + checked: _orientation == Axis.vertical, + onChanged: (v) { + if (v) setState(() => _orientation = Axis.vertical); + }, + content: const Text('Vertical'), + ), + const SizedBox(width: 20), + RadioButton( + checked: _orientation == Axis.horizontal, + onChanged: (v) { + if (v) setState(() => _orientation = Axis.horizontal); + }, + content: const Text('Horizontal'), + ), + ]), + const SizedBox(height: 20), + Text('Options:', + style: FluentTheme.of(context).typography.bodyStrong), + const SizedBox(height: 8), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + Checkbox( + checked: _isColorPreviewVisible, + onChanged: (v) => setState(() => _isColorPreviewVisible = v!), + content: const Text('Color Preview'), + ), + Checkbox( + checked: _isColorSliderVisible, + onChanged: (v) => setState(() => _isColorSliderVisible = v!), + content: const Text('Color Slider'), + ), + if (_orientation == Axis.vertical) ...[ + Checkbox( + checked: _isMoreButtonVisible, + onChanged: (v) => setState(() => _isMoreButtonVisible = v!), + content: const Text('More Button'), + ), + ], + Checkbox( + checked: _isColorChannelTextInputVisible, + onChanged: (v) => + setState(() => _isColorChannelTextInputVisible = v!), + content: const Text('Channel Text Input'), + ), + Checkbox( + checked: _isHexInputVisible, + onChanged: (v) => setState(() => _isHexInputVisible = v!), + content: const Text('Hex Input'), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 10, + runSpacing: 10, + children: [ + Checkbox( + checked: _isAlphaEnabled, + onChanged: (v) => setState(() { + _isAlphaEnabled = v!; + if (!v) { + _isAlphaSliderVisible = false; + _isAlphaTextInputVisible = false; + } + }), + content: const Text('Alpha Enabled'), + ), + if (_isAlphaEnabled) ...[ + Checkbox( + checked: _isAlphaSliderVisible, + onChanged: (v) => + setState(() => _isAlphaSliderVisible = v!), + content: const Text('Alpha Slider'), + ), + Checkbox( + checked: _isAlphaTextInputVisible, + onChanged: (v) => + setState(() => _isAlphaTextInputVisible = v!), + content: const Text('Alpha Text Input'), + ), + ], + ], + ), + const SizedBox(height: 20), + Text('Selected Color:', + style: FluentTheme.of(context).typography.bodyStrong), + const SizedBox(height: 8), + Container( + color: _selectedColor, + width: 200, + height: 50, + ), + ], + ), + const SizedBox(height: 20), + subtitle(content: const Text('ColorPicker Demo')), + CardHighlight( + codeSnippet: '''Color selectedColor = Colors.blue; +ColorSpectrumShape spectrumShape = ColorSpectrumShape.box; + +ColorPicker( + color: selectedColor, + onChanged: (color) => setState(() => selectedColor = color), + colorSpectrumShape: spectrumShape, + isMoreButtonVisible: true, + isColorSliderVisible: true, + isColorChannelTextInputVisible: true, + isHexInputVisible: true, + isAlphaEnabled: false, +),''', + child: Row(children: [ + ColorPicker( + color: _selectedColor, + onChanged: (color) => setState(() => _selectedColor = color), + colorSpectrumShape: _spectrumShape, + orientation: _orientation, + isMoreButtonVisible: _isMoreButtonVisible, + isColorSliderVisible: _isColorSliderVisible, + isColorChannelTextInputVisible: _isColorChannelTextInputVisible, + isHexInputVisible: _isHexInputVisible, + isColorPreviewVisible: _isColorPreviewVisible, + isAlphaEnabled: _isAlphaEnabled, + isAlphaSliderVisible: _isAlphaSliderVisible, + isAlphaTextInputVisible: _isAlphaTextInputVisible, + ), + ]), + ), + ], + ); + } +} diff --git a/lib/fluent_ui.dart b/lib/fluent_ui.dart index 83ef48c38..6dd39bed7 100644 --- a/lib/fluent_ui.dart +++ b/lib/fluent_ui.dart @@ -49,6 +49,7 @@ export 'src/controls/form/number_box.dart'; export 'src/controls/form/password_box.dart'; export 'src/controls/form/pickers/date_picker.dart'; export 'src/controls/form/pickers/time_picker.dart'; +export 'src/controls/form/color_picker/color_picker.dart'; export 'src/controls/form/selection_controls.dart'; export 'src/controls/form/text_box.dart'; export 'src/controls/form/text_form_box.dart'; diff --git a/lib/src/controls/form/color_picker/color_names.dart b/lib/src/controls/form/color_picker/color_names.dart new file mode 100644 index 000000000..9087fdac9 --- /dev/null +++ b/lib/src/controls/form/color_picker/color_names.dart @@ -0,0 +1,1575 @@ +part of "color_state.dart"; + +/// Contains color names adapted from "Name That Color" +/// See: http://chir.ag/projects/name-that-color +class _ColorNames { + /// A map of color names and their corresponding hex values. + static const Map _values = { + 0xFF000000: "Black", + 0xFF000080: "Navy Blue", + 0xFF0000C8: "Dark Blue", + 0xFF0000FF: "Blue", + 0xFF000741: "Stratos", + 0xFF001B1C: "Swamp", + 0xFF002387: "Resolution Blue", + 0xFF002900: "Deep Fir", + 0xFF002E20: "Burnham", + 0xFF002FA7: "International Klein Blue", + 0xFF003153: "Prussian Blue", + 0xFF003366: "Midnight Blue", + 0xFF003399: "Smalt", + 0xFF003532: "Deep Teal", + 0xFF003E40: "Cyprus", + 0xFF004620: "Kaitoke Green", + 0xFF0047AB: "Cobalt", + 0xFF004816: "Crusoe", + 0xFF004950: "Sherpa Blue", + 0xFF0056A7: "Endeavour", + 0xFF00581A: "Camarone", + 0xFF0066CC: "Science Blue", + 0xFF0066FF: "Blue Ribbon", + 0xFF00755E: "Tropical Rain Forest", + 0xFF0076A3: "Allports", + 0xFF007BA7: "Deep Cerulean", + 0xFF007EC7: "Lochmara", + 0xFF007FFF: "Azure Radiance", + 0xFF008080: "Teal", + 0xFF0095B6: "Bondi Blue", + 0xFF009DC4: "Pacific Blue", + 0xFF00A693: "Persian Green", + 0xFF00A86B: "Jade", + 0xFF00CC99: "Caribbean Green", + 0xFF00CCCC: "Robin's Egg Blue", + 0xFF00FF00: "Green", + 0xFF00FF7F: "Spring Green", + 0xFF00FFFF: "Cyan / Aqua", + 0xFF010D1A: "Blue Charcoal", + 0xFF011635: "Midnight", + 0xFF011D13: "Holly", + 0xFF012731: "Daintree", + 0xFF01361C: "Cardin Green", + 0xFF01371A: "County Green", + 0xFF013E62: "Astronaut Blue", + 0xFF013F6A: "Regal Blue", + 0xFF014B43: "Aqua Deep", + 0xFF015E85: "Orient", + 0xFF016162: "Blue Stone", + 0xFF016D39: "Fun Green", + 0xFF01796F: "Pine Green", + 0xFF017987: "Blue Lagoon", + 0xFF01826B: "Deep Sea", + 0xFF01A368: "Green Haze", + 0xFF022D15: "English Holly", + 0xFF02402C: "Sherwood Green", + 0xFF02478E: "Congress Blue", + 0xFF024E46: "Evening Sea", + 0xFF026395: "Bahama Blue", + 0xFF02866F: "Observatory", + 0xFF02A4D3: "Cerulean", + 0xFF03163C: "Tangaroa", + 0xFF032B52: "Green Vogue", + 0xFF036A6E: "Mosque", + 0xFF041004: "Midnight Moss", + 0xFF041322: "Black Pearl", + 0xFF042E4C: "Blue Whale", + 0xFF044022: "Zuccini", + 0xFF044259: "Teal Blue", + 0xFF051040: "Deep Cove", + 0xFF051657: "Gulf Blue", + 0xFF055989: "Venice Blue", + 0xFF056F57: "Watercourse", + 0xFF062A78: "Catalina Blue", + 0xFF063537: "Tiber", + 0xFF069B81: "Gossamer", + 0xFF06A189: "Niagara", + 0xFF073A50: "Tarawera", + 0xFF080110: "Jaguar", + 0xFF081910: "Black Bean", + 0xFF082567: "Deep Sapphire", + 0xFF088370: "Elf Green", + 0xFF08E8DE: "Bright Turquoise", + 0xFF092256: "Downriver", + 0xFF09230F: "Palm Green", + 0xFF09255D: "Madison", + 0xFF093624: "Bottle Green", + 0xFF095859: "Deep Sea Green", + 0xFF097F4B: "Salem", + 0xFF0A001C: "Black Russian", + 0xFF0A480D: "Dark Fern", + 0xFF0A6906: "Japanese Laurel", + 0xFF0A6F75: "Atoll", + 0xFF0B0B0B: "Cod Gray", + 0xFF0B0F08: "Marshland", + 0xFF0B1107: "Gordons Green", + 0xFF0B1304: "Black Forest", + 0xFF0B6207: "San Felix", + 0xFF0BDA51: "Malachite", + 0xFF0C0B1D: "Ebony", + 0xFF0C0D0F: "Woodsmoke", + 0xFF0C1911: "Racing Green", + 0xFF0C7A79: "Surfie Green", + 0xFF0C8990: "Blue Chill", + 0xFF0D0332: "Black Rock", + 0xFF0D1117: "Bunker", + 0xFF0D1C19: "Aztec", + 0xFF0D2E1C: "Bush", + 0xFF0E0E18: "Cinder", + 0xFF0E2A30: "Firefly", + 0xFF0F2D9E: "Torea Bay", + 0xFF10121D: "Vulcan", + 0xFF101405: "Green Waterloo", + 0xFF105852: "Eden", + 0xFF110C6C: "Arapawa", + 0xFF120A8F: "Ultramarine", + 0xFF123447: "Elephant", + 0xFF126B40: "Jewel", + 0xFF130000: "Diesel", + 0xFF130A06: "Asphalt", + 0xFF13264D: "Blue Zodiac", + 0xFF134F19: "Parsley", + 0xFF140600: "Nero", + 0xFF1450AA: "Tory Blue", + 0xFF151F4C: "Bunting", + 0xFF1560BD: "Denim", + 0xFF15736B: "Genoa", + 0xFF161928: "Mirage", + 0xFF161D10: "Hunter Green", + 0xFF162A40: "Big Stone", + 0xFF163222: "Celtic", + 0xFF16322C: "Timber Green", + 0xFF163531: "Gable Green", + 0xFF171F04: "Pine Tree", + 0xFF175579: "Chathams Blue", + 0xFF182D09: "Deep Forest Green", + 0xFF18587A: "Blumine", + 0xFF19330E: "Palm Leaf", + 0xFF193751: "Nile Blue", + 0xFF1959A8: "Fun Blue", + 0xFF1A1A68: "Lucky Point", + 0xFF1AB385: "Mountain Meadow", + 0xFF1B0245: "Tolopea", + 0xFF1B1035: "Haiti", + 0xFF1B127B: "Deep Koamaru", + 0xFF1B1404: "Acadia", + 0xFF1B2F11: "Seaweed", + 0xFF1B3162: "Biscay", + 0xFF1B659D: "Matisse", + 0xFF1C1208: "Crowshead", + 0xFF1C1E13: "Rangoon Green", + 0xFF1C39BB: "Persian Blue", + 0xFF1C402E: "Everglade", + 0xFF1C7C7D: "Elm", + 0xFF1D6142: "Green Pea", + 0xFF1E0F04: "Creole", + 0xFF1E1609: "Karaka", + 0xFF1E1708: "El Paso", + 0xFF1E385B: "Cello", + 0xFF1E433C: "Te Papa Green", + 0xFF1E90FF: "Dodger Blue", + 0xFF1E9AB0: "Eastern Blue", + 0xFF1F120F: "Night Rider", + 0xFF1FC2C2: "Java", + 0xFF20208D: "Jacksons Purple", + 0xFF202E54: "Cloud Burst", + 0xFF204852: "Blue Dianne", + 0xFF211A0E: "Eternity", + 0xFF220878: "Deep Blue", + 0xFF228B22: "Forest Green", + 0xFF233418: "Mallard", + 0xFF240A40: "Violet", + 0xFF240C02: "Kilamanjaro", + 0xFF242A1D: "Log Cabin", + 0xFF242E16: "Black Olive", + 0xFF24500F: "Green House", + 0xFF251607: "Graphite", + 0xFF251706: "Cannon Black", + 0xFF251F4F: "Port Gore", + 0xFF25272C: "Shark", + 0xFF25311C: "Green Kelp", + 0xFF2596D1: "Curious Blue", + 0xFF260368: "Paua", + 0xFF26056A: "Paris M", + 0xFF261105: "Wood Bark", + 0xFF261414: "Gondola", + 0xFF262335: "Steel Gray", + 0xFF26283B: "Ebony Clay", + 0xFF273A81: "Bay of Many", + 0xFF27504B: "Plantation", + 0xFF278A5B: "Eucalyptus", + 0xFF281E15: "Oil", + 0xFF283A77: "Astronaut", + 0xFF286ACD: "Mariner", + 0xFF290C5E: "Violent Violet", + 0xFF292130: "Bastille", + 0xFF292319: "Zeus", + 0xFF292937: "Charade", + 0xFF297B9A: "Jelly Bean", + 0xFF29AB87: "Jungle Green", + 0xFF2A0359: "Cherry Pie", + 0xFF2A140E: "Coffee Bean", + 0xFF2A2630: "Baltic Sea", + 0xFF2A380B: "Turtle Green", + 0xFF2A52BE: "Cerulean Blue", + 0xFF2B0202: "Sepia Black", + 0xFF2B194F: "Valhalla", + 0xFF2B3228: "Heavy Metal", + 0xFF2C0E8C: "Blue Gem", + 0xFF2C1632: "Revolver", + 0xFF2C2133: "Bleached Cedar", + 0xFF2C8C84: "Lochinvar", + 0xFF2D2510: "Mikado", + 0xFF2D383A: "Outer Space", + 0xFF2D569B: "St Tropaz", + 0xFF2E0329: "Jacaranda", + 0xFF2E1905: "Jacko Bean", + 0xFF2E3222: "Rangitoto", + 0xFF2E3F62: "Rhino", + 0xFF2E8B57: "Sea Green", + 0xFF2EBFD4: "Scooter", + 0xFF2F270E: "Onion", + 0xFF2F3CB3: "Governor Bay", + 0xFF2F519E: "Sapphire", + 0xFF2F5A57: "Spectra", + 0xFF2F6168: "Casal", + 0xFF300529: "Melanzane", + 0xFF301F1E: "Cocoa Brown", + 0xFF302A0F: "Woodrush", + 0xFF304B6A: "San Juan", + 0xFF30D5C8: "Turquoise", + 0xFF311C17: "Eclipse", + 0xFF314459: "Pickled Bluewood", + 0xFF315BA1: "Azure", + 0xFF31728D: "Calypso", + 0xFF317D82: "Paradiso", + 0xFF32127A: "Persian Indigo", + 0xFF32293A: "Blackcurrant", + 0xFF323232: "Mine Shaft", + 0xFF325D52: "Stromboli", + 0xFF327C14: "Bilbao", + 0xFF327DA0: "Astral", + 0xFF33036B: "Christalle", + 0xFF33292F: "Thunder", + 0xFF33CC99: "Shamrock", + 0xFF341515: "Tamarind", + 0xFF350036: "Mardi Gras", + 0xFF350E42: "Valentino", + 0xFF350E57: "Jagger", + 0xFF353542: "Tuna", + 0xFF354E8C: "Chambray", + 0xFF363050: "Martinique", + 0xFF363534: "Tuatara", + 0xFF363C0D: "Waiouru", + 0xFF36747D: "Ming", + 0xFF368716: "La Palma", + 0xFF370202: "Chocolate", + 0xFF371D09: "Clinker", + 0xFF37290E: "Brown Tumbleweed", + 0xFF373021: "Birch", + 0xFF377475: "Oracle", + 0xFF380474: "Blue Diamond", + 0xFF381A51: "Grape", + 0xFF383533: "Dune", + 0xFF384555: "Oxford Blue", + 0xFF384910: "Clover", + 0xFF394851: "Limed Spruce", + 0xFF396413: "Dell", + 0xFF3A0020: "Toledo", + 0xFF3A2010: "Sambuca", + 0xFF3A2A6A: "Jacarta", + 0xFF3A686C: "William", + 0xFF3A6A47: "Killarney", + 0xFF3AB09E: "Keppel", + 0xFF3B000B: "Temptress", + 0xFF3B0910: "Aubergine", + 0xFF3B1F1F: "Jon", + 0xFF3B2820: "Treehouse", + 0xFF3B7A57: "Amazon", + 0xFF3B91B4: "Boston Blue", + 0xFF3C0878: "Windsor", + 0xFF3C1206: "Rebel", + 0xFF3C1F76: "Meteorite", + 0xFF3C2005: "Dark Ebony", + 0xFF3C3910: "Camouflage", + 0xFF3C4151: "Bright Gray", + 0xFF3C4443: "Cape Cod", + 0xFF3C493A: "Lunar Green", + 0xFF3D0C02: "Bean ", + 0xFF3D2B1F: "Bistre", + 0xFF3D7D52: "Goblin", + 0xFF3E0480: "Kingfisher Daisy", + 0xFF3E1C14: "Cedar", + 0xFF3E2B23: "English Walnut", + 0xFF3E2C1C: "Black Marlin", + 0xFF3E3A44: "Ship Gray", + 0xFF3EABBF: "Pelorous", + 0xFF3F2109: "Bronze", + 0xFF3F2500: "Cola", + 0xFF3F3002: "Madras", + 0xFF3F307F: "Minsk", + 0xFF3F4C3A: "Cabbage Pont", + 0xFF3F583B: "Tom Thumb", + 0xFF3F5D53: "Mineral Green", + 0xFF3FC1AA: "Puerto Rico", + 0xFF3FFF00: "Harlequin", + 0xFF401801: "Brown Pod", + 0xFF40291D: "Cork", + 0xFF403B38: "Masala", + 0xFF403D19: "Thatch Green", + 0xFF405169: "Fiord", + 0xFF40826D: "Viridian", + 0xFF40A860: "Chateau Green", + 0xFF410056: "Ripe Plum", + 0xFF411F10: "Paco", + 0xFF412010: "Deep Oak", + 0xFF413C37: "Merlin", + 0xFF414257: "Gun Powder", + 0xFF414C7D: "East Bay", + 0xFF4169E1: "Royal Blue", + 0xFF41AA78: "Ocean Green", + 0xFF420303: "Burnt Maroon", + 0xFF423921: "Lisbon Brown", + 0xFF427977: "Faded Jade", + 0xFF431560: "Scarlet Gum", + 0xFF433120: "Iroko", + 0xFF433E37: "Armadillo", + 0xFF434C59: "River Bed", + 0xFF436A0D: "Green Leaf", + 0xFF44012D: "Barossa", + 0xFF441D00: "Morocco Brown", + 0xFF444954: "Mako", + 0xFF454936: "Kelp", + 0xFF456CAC: "San Marino", + 0xFF45B1E8: "Picton Blue", + 0xFF460B41: "Loulou", + 0xFF462425: "Crater Brown", + 0xFF465945: "Gray Asparagus", + 0xFF4682B4: "Steel Blue", + 0xFF480404: "Rustic Red", + 0xFF480607: "Bulgarian Rose", + 0xFF480656: "Clairvoyant", + 0xFF481C1C: "Cocoa Bean", + 0xFF483131: "Woody Brown", + 0xFF483C32: "Taupe", + 0xFF49170C: "Van Cleef", + 0xFF492615: "Brown Derby", + 0xFF49371B: "Metallic Bronze", + 0xFF495400: "Verdun Green", + 0xFF496679: "Blue Bayoux", + 0xFF497183: "Bismark", + 0xFF4A2A04: "Bracken", + 0xFF4A3004: "Deep Bronze", + 0xFF4A3C30: "Mondo", + 0xFF4A4244: "Tundora", + 0xFF4A444B: "Gravel", + 0xFF4A4E5A: "Trout", + 0xFF4B0082: "Pigment Indigo", + 0xFF4B5D52: "Nandor", + 0xFF4C3024: "Saddle", + 0xFF4C4F56: "Abbey", + 0xFF4D0135: "Blackberry", + 0xFF4D0A18: "Cab Sav", + 0xFF4D1E01: "Indian Tan", + 0xFF4D282D: "Cowboy", + 0xFF4D282E: "Livid Brown", + 0xFF4D3833: "Rock", + 0xFF4D3D14: "Punga", + 0xFF4D400F: "Bronzetone", + 0xFF4D5328: "Woodland", + 0xFF4E0606: "Mahogany", + 0xFF4E2A5A: "Bossanova", + 0xFF4E3B41: "Matterhorn", + 0xFF4E420C: "Bronze Olive", + 0xFF4E4562: "Mulled Wine", + 0xFF4E6649: "Axolotl", + 0xFF4E7F9E: "Wedgewood", + 0xFF4EABD1: "Shakespeare", + 0xFF4F1C70: "Honey Flower", + 0xFF4F2398: "Daisy Bush", + 0xFF4F69C6: "Indigo", + 0xFF4F7942: "Fern Green", + 0xFF4F9D5D: "Fruit Salad", + 0xFF4FA83D: "Apple", + 0xFF504351: "Mortar", + 0xFF507096: "Kashmir Blue", + 0xFF507672: "Cutty Sark", + 0xFF50C878: "Emerald", + 0xFF514649: "Emperor", + 0xFF516E3D: "Chalet Green", + 0xFF517C66: "Como", + 0xFF51808F: "Smalt Blue", + 0xFF52001F: "Castro", + 0xFF520C17: "Maroon Oak", + 0xFF523C94: "Gigas", + 0xFF533455: "Voodoo", + 0xFF534491: "Victoria", + 0xFF53824B: "Hippie Green", + 0xFF541012: "Heath", + 0xFF544333: "Judge Gray", + 0xFF54534D: "Fuscous Gray", + 0xFF549019: "Vida Loca", + 0xFF55280C: "Cioccolato", + 0xFF555B10: "Saratoga", + 0xFF556D56: "Finlandia", + 0xFF5590D9: "Havelock Blue", + 0xFF56B4BE: "Fountain Blue", + 0xFF578363: "Spring Leaves", + 0xFF583401: "Saddle Brown", + 0xFF585562: "Scarpa Flow", + 0xFF587156: "Cactus", + 0xFF589AAF: "Hippie Blue", + 0xFF591D35: "Wine Berry", + 0xFF592804: "Brown Bramble", + 0xFF593737: "Congo Brown", + 0xFF594433: "Millbrook", + 0xFF5A6E9C: "Waikawa Gray", + 0xFF5A87A0: "Horizon", + 0xFF5B3013: "Jambalaya", + 0xFF5C0120: "Bordeaux", + 0xFF5C0536: "Mulberry Wood", + 0xFF5C2E01: "Carnaby Tan", + 0xFF5C5D75: "Comet", + 0xFF5D1E0F: "Redwood", + 0xFF5D4C51: "Don Juan", + 0xFF5D5C58: "Chicago", + 0xFF5D5E37: "Verdigris", + 0xFF5D7747: "Dingley", + 0xFF5DA19F: "Breaker Bay", + 0xFF5E483E: "Kabul", + 0xFF5E5D3B: "Hemlock", + 0xFF5F3D26: "Irish Coffee", + 0xFF5F5F6E: "Mid Gray", + 0xFF5F6672: "Shuttle Gray", + 0xFF5FA777: "Aqua Forest", + 0xFF5FB3AC: "Tradewind", + 0xFF604913: "Horses Neck", + 0xFF605B73: "Smoky", + 0xFF606E68: "Corduroy", + 0xFF6093D1: "Danube", + 0xFF612718: "Espresso", + 0xFF614051: "Eggplant", + 0xFF615D30: "Costa Del Sol", + 0xFF61845F: "Glade Green", + 0xFF622F30: "Buccaneer", + 0xFF623F2D: "Quincy", + 0xFF624E9A: "Butterfly Bush", + 0xFF625119: "West Coast", + 0xFF626649: "Finch", + 0xFF639A8F: "Patina", + 0xFF63B76C: "Fern", + 0xFF6456B7: "Blue Violet", + 0xFF646077: "Dolphin", + 0xFF646463: "Storm Dust", + 0xFF646A54: "Siam", + 0xFF646E75: "Nevada", + 0xFF6495ED: "Cornflower Blue", + 0xFF64CCDB: "Viking", + 0xFF65000B: "Rosewood", + 0xFF651A14: "Cherrywood", + 0xFF652DC1: "Purple Heart", + 0xFF657220: "Fern Frond", + 0xFF65745D: "Willow Grove", + 0xFF65869F: "Hoki", + 0xFF660045: "Pompadour", + 0xFF660099: "Purple", + 0xFF66023C: "Tyrian Purple", + 0xFF661010: "Dark Tan", + 0xFF66B58F: "Silver Tree", + 0xFF66FF00: "Bright Green", + 0xFF66FF66: "Screamin' Green", + 0xFF67032D: "Black Rose", + 0xFF675FA6: "Scampi", + 0xFF676662: "Ironside Gray", + 0xFF678975: "Viridian Green", + 0xFF67A712: "Christi", + 0xFF683600: "Nutmeg Wood Finish", + 0xFF685558: "Zambezi", + 0xFF685E6E: "Salt Box", + 0xFF692545: "Tawny Port", + 0xFF692D54: "Finn", + 0xFF695F62: "Scorpion", + 0xFF697E9A: "Lynch", + 0xFF6A442E: "Spice", + 0xFF6A5D1B: "Himalaya", + 0xFF6A6051: "Soya Bean", + 0xFF6B2A14: "Hairy Heath", + 0xFF6B3FA0: "Royal Purple", + 0xFF6B4E31: "Shingle Fawn", + 0xFF6B5755: "Dorado", + 0xFF6B8BA2: "Bermuda Gray", + 0xFF6B8E23: "Olive Drab", + 0xFF6C3082: "Eminence", + 0xFF6CDAE7: "Turquoise Blue", + 0xFF6D0101: "Lonestar", + 0xFF6D5E54: "Pine Cone", + 0xFF6D6C6C: "Dove Gray", + 0xFF6D9292: "Juniper", + 0xFF6D92A1: "Gothic", + 0xFF6E0902: "Red Oxide", + 0xFF6E1D14: "Moccaccino", + 0xFF6E4826: "Pickled Bean", + 0xFF6E4B26: "Dallas", + 0xFF6E6D57: "Kokoda", + 0xFF6E7783: "Pale Sky", + 0xFF6F440C: "Cafe Royale", + 0xFF6F6A61: "Flint", + 0xFF6F8E63: "Highland", + 0xFF6F9D02: "Limeade", + 0xFF6FD0C5: "Downy", + 0xFF701C1C: "Persian Plum", + 0xFF704214: "Sepia", + 0xFF704A07: "Antique Bronze", + 0xFF704F50: "Ferra", + 0xFF706555: "Coffee", + 0xFF708090: "Slate Gray", + 0xFF711A00: "Cedar Wood Finish", + 0xFF71291D: "Metallic Copper", + 0xFF714693: "Affair", + 0xFF714AB2: "Studio", + 0xFF715D47: "Tobacco Brown", + 0xFF716338: "Yellow Metal", + 0xFF716B56: "Peat", + 0xFF716E10: "Olivetone", + 0xFF717486: "Storm Gray", + 0xFF718080: "Sirocco", + 0xFF71D9E2: "Aquamarine Blue", + 0xFF72010F: "Venetian Red", + 0xFF724A2F: "Old Copper", + 0xFF726D4E: "Go Ben", + 0xFF727B89: "Raven", + 0xFF731E8F: "Seance", + 0xFF734A12: "Raw Umber", + 0xFF736C9F: "Kimberly", + 0xFF736D58: "Crocodile", + 0xFF737829: "Crete", + 0xFF738678: "Xanadu", + 0xFF74640D: "Spicy Mustard", + 0xFF747D63: "Limed Ash", + 0xFF747D83: "Rolling Stone", + 0xFF748881: "Blue Smoke", + 0xFF749378: "Laurel", + 0xFF74C365: "Mantis", + 0xFF755A57: "Russett", + 0xFF7563A8: "Deluge", + 0xFF76395D: "Cosmic", + 0xFF7666C6: "Blue Marguerite", + 0xFF76BD17: "Lima", + 0xFF76D7EA: "Sky Blue", + 0xFF770F05: "Dark Burgundy", + 0xFF771F1F: "Crown of Thorns", + 0xFF773F1A: "Walnut", + 0xFF776F61: "Pablo", + 0xFF778120: "Pacifika", + 0xFF779E86: "Oxley", + 0xFF77DD77: "Pastel Green", + 0xFF780109: "Japanese Maple", + 0xFF782D19: "Mocha", + 0xFF782F16: "Peanut", + 0xFF78866B: "Camouflage Green", + 0xFF788A25: "Wasabi", + 0xFF788BBA: "Ship Cove", + 0xFF78A39C: "Sea Nymph", + 0xFF795D4C: "Roman Coffee", + 0xFF796878: "Old Lavender", + 0xFF796989: "Rum", + 0xFF796A78: "Fedora", + 0xFF796D62: "Sandstone", + 0xFF79DEEC: "Spray", + 0xFF7A013A: "Siren", + 0xFF7A58C1: "Fuchsia Blue", + 0xFF7A7A7A: "Boulder", + 0xFF7A89B8: "Wild Blue Yonder", + 0xFF7AC488: "De York", + 0xFF7B3801: "Red Beech", + 0xFF7B3F00: "Cinnamon", + 0xFF7B6608: "Yukon Gold", + 0xFF7B7874: "Tapa", + 0xFF7B7C94: "Waterloo ", + 0xFF7B8265: "Flax Smoke", + 0xFF7B9F80: "Amulet", + 0xFF7BA05B: "Asparagus", + 0xFF7C1C05: "Kenyan Copper", + 0xFF7C7631: "Pesto", + 0xFF7C778A: "Topaz", + 0xFF7C7B7A: "Concord", + 0xFF7C7B82: "Jumbo", + 0xFF7C881A: "Trendy Green", + 0xFF7CA1A6: "Gumbo", + 0xFF7CB0A1: "Acapulco", + 0xFF7CB7BB: "Neptune", + 0xFF7D2C14: "Pueblo", + 0xFF7DA98D: "Bay Leaf", + 0xFF7DC8F7: "Malibu", + 0xFF7DD8C6: "Bermuda", + 0xFF7E3A15: "Copper Canyon", + 0xFF7F1734: "Claret", + 0xFF7F3A02: "Peru Tan", + 0xFF7F626D: "Falcon", + 0xFF7F7589: "Mobster", + 0xFF7F76D3: "Moody Blue", + 0xFF7FFF00: "Chartreuse", + 0xFF7FFFD4: "Aquamarine", + 0xFF800000: "Maroon", + 0xFF800B47: "Rose Bud Cherry", + 0xFF801818: "Falu Red", + 0xFF80341F: "Red Robin", + 0xFF803790: "Vivid Violet", + 0xFF80461B: "Russet", + 0xFF807E79: "Friar Gray", + 0xFF808000: "Olive", + 0xFF808080: "Gray", + 0xFF80B3AE: "Gulf Stream", + 0xFF80B3C4: "Glacier", + 0xFF80CCEA: "Seagull", + 0xFF81422C: "Nutmeg", + 0xFF816E71: "Spicy Pink", + 0xFF817377: "Empress", + 0xFF819885: "Spanish Green", + 0xFF826F65: "Sand Dune", + 0xFF828685: "Gunsmoke", + 0xFF828F72: "Battleship Gray", + 0xFF831923: "Merlot", + 0xFF837050: "Shadow", + 0xFF83AA5D: "Chelsea Cucumber", + 0xFF83D0C6: "Monte Carlo", + 0xFF843179: "Plum", + 0xFF84A0A0: "Granny Smith", + 0xFF8581D9: "Chetwode Blue", + 0xFF858470: "Bandicoot", + 0xFF859FAF: "Bali Hai", + 0xFF85C4CC: "Half Baked", + 0xFF860111: "Red Devil", + 0xFF863C3C: "Lotus", + 0xFF86483C: "Ironstone", + 0xFF864D1E: "Bull Shot", + 0xFF86560A: "Rusty Nail", + 0xFF868974: "Bitter", + 0xFF86949F: "Regent Gray", + 0xFF871550: "Disco", + 0xFF87756E: "Americano", + 0xFF877C7B: "Hurricane", + 0xFF878D91: "Oslo Gray", + 0xFF87AB39: "Sushi", + 0xFF885342: "Spicy Mix", + 0xFF886221: "Kumera", + 0xFF888387: "Suva Gray", + 0xFF888D65: "Avocado", + 0xFF893456: "Camelot", + 0xFF893843: "Solid Pink", + 0xFF894367: "Cannon Pink", + 0xFF897D6D: "Makara", + 0xFF8A3324: "Burnt Umber", + 0xFF8A73D6: "True V", + 0xFF8A8360: "Clay Creek", + 0xFF8A8389: "Monsoon", + 0xFF8A8F8A: "Stack", + 0xFF8AB9F1: "Jordy Blue", + 0xFF8B00FF: "Electric Violet", + 0xFF8B0723: "Monarch", + 0xFF8B6B0B: "Corn Harvest", + 0xFF8B8470: "Olive Haze", + 0xFF8B847E: "Schooner", + 0xFF8B8680: "Natural Gray", + 0xFF8B9C90: "Mantle", + 0xFF8B9FEE: "Portage", + 0xFF8BA690: "Envy", + 0xFF8BA9A5: "Cascade", + 0xFF8BE6D8: "Riptide", + 0xFF8C055E: "Cardinal Pink", + 0xFF8C472F: "Mule Fawn", + 0xFF8C5738: "Potters Clay", + 0xFF8C6495: "Trendy Pink", + 0xFF8D0226: "Paprika", + 0xFF8D3D38: "Sanguine Brown", + 0xFF8D3F3F: "Tosca", + 0xFF8D7662: "Cement", + 0xFF8D8974: "Granite Green", + 0xFF8D90A1: "Manatee", + 0xFF8DA8CC: "Polo Blue", + 0xFF8E0000: "Red Berry", + 0xFF8E4D1E: "Rope", + 0xFF8E6F70: "Opium", + 0xFF8E775E: "Domino", + 0xFF8E8190: "Mamba", + 0xFF8EABC1: "Nepal", + 0xFF8F021C: "Pohutukawa", + 0xFF8F3E33: "El Salva", + 0xFF8F4B0E: "Korma", + 0xFF8F8176: "Squirrel", + 0xFF8FD6B4: "Vista Blue", + 0xFF900020: "Burgundy", + 0xFF901E1E: "Old Brick", + 0xFF907874: "Hemp", + 0xFF907B71: "Almond Frost", + 0xFF908D39: "Sycamore", + 0xFF92000A: "Sangria", + 0xFF924321: "Cumin", + 0xFF926F5B: "Beaver", + 0xFF928573: "Stonewall", + 0xFF928590: "Venus", + 0xFF9370DB: "Medium Purple", + 0xFF93CCEA: "Cornflower", + 0xFF93DFB8: "Algae Green", + 0xFF944747: "Copper Rust", + 0xFF948771: "Arrowtown", + 0xFF950015: "Scarlett", + 0xFF956387: "Strikemaster", + 0xFF959396: "Mountain Mist", + 0xFF960018: "Carmine", + 0xFF964B00: "Brown", + 0xFF967059: "Leather", + 0xFF9678B6: "Purple Mountain's Majesty", + 0xFF967BB6: "Lavender Purple", + 0xFF96A8A1: "Pewter", + 0xFF96BBAB: "Summer Green", + 0xFF97605D: "Au Chico", + 0xFF9771B5: "Wisteria", + 0xFF97CD2D: "Atlantis", + 0xFF983D61: "Vin Rouge", + 0xFF9874D3: "Lilac Bush", + 0xFF98777B: "Bazaar", + 0xFF98811B: "Hacienda", + 0xFF988D77: "Pale Oyster", + 0xFF98FF98: "Mint Green", + 0xFF990066: "Fresh Eggplant", + 0xFF991199: "Violet Eggplant", + 0xFF991613: "Tamarillo", + 0xFF991B07: "Totem Pole", + 0xFF996666: "Copper Rose", + 0xFF9966CC: "Amethyst", + 0xFF997A8D: "Mountbatten Pink", + 0xFF9999CC: "Blue Bell", + 0xFF9A3820: "Prairie Sand", + 0xFF9A6E61: "Toast", + 0xFF9A9577: "Gurkha", + 0xFF9AB973: "Olivine", + 0xFF9AC2B8: "Shadow Green", + 0xFF9B4703: "Oregon", + 0xFF9B9E8F: "Lemon Grass", + 0xFF9C3336: "Stiletto", + 0xFF9D5616: "Hawaiian Tan", + 0xFF9DACB7: "Gull Gray", + 0xFF9DC209: "Pistachio", + 0xFF9DE093: "Granny Smith Apple", + 0xFF9DE5FF: "Anakiwa", + 0xFF9E5302: "Chelsea Gem", + 0xFF9E5B40: "Sepia Skin", + 0xFF9EA587: "Sage", + 0xFF9EA91F: "Citron", + 0xFF9EB1CD: "Rock Blue", + 0xFF9EDEE0: "Morning Glory", + 0xFF9F381D: "Cognac", + 0xFF9F821C: "Reef Gold", + 0xFF9F9F9C: "Star Dust", + 0xFF9FA0B1: "Santas Gray", + 0xFF9FD7D3: "Sinbad", + 0xFF9FDD8C: "Feijoa", + 0xFFA02712: "Tabasco", + 0xFFA1750D: "Buttered Rum", + 0xFFA1ADB5: "Hit Gray", + 0xFFA1C50A: "Citrus", + 0xFFA1DAD7: "Aqua Island", + 0xFFA1E9DE: "Water Leaf", + 0xFFA2006D: "Flirt", + 0xFFA23B6C: "Rouge", + 0xFFA26645: "Cape Palliser", + 0xFFA2AAB3: "Gray Chateau", + 0xFFA2AEAB: "Edward", + 0xFFA3807B: "Pharlap", + 0xFFA397B4: "Amethyst Smoke", + 0xFFA3E3ED: "Blizzard Blue", + 0xFFA4A49D: "Delta", + 0xFFA4A6D3: "Wistful", + 0xFFA4AF6E: "Green Smoke", + 0xFFA50B5E: "Jazzberry Jam", + 0xFFA59B91: "Zorba", + 0xFFA5CB0C: "Bahia", + 0xFFA62F20: "Roof Terracotta", + 0xFFA65529: "Paarl", + 0xFFA68B5B: "Barley Corn", + 0xFFA69279: "Donkey Brown", + 0xFFA6A29A: "Dawn", + 0xFFA72525: "Mexican Red", + 0xFFA7882C: "Luxor Gold", + 0xFFA85307: "Rich Gold", + 0xFFA86515: "Reno Sand", + 0xFFA86B6B: "Coral Tree", + 0xFFA8989B: "Dusty Gray", + 0xFFA899E6: "Dull Lavender", + 0xFFA8A589: "Tallow", + 0xFFA8AE9C: "Bud", + 0xFFA8AF8E: "Locust", + 0xFFA8BD9F: "Norway", + 0xFFA8E3BD: "Chinook", + 0xFFA9A491: "Gray Olive", + 0xFFA9ACB6: "Aluminium", + 0xFFA9B2C3: "Cadet Blue", + 0xFFA9B497: "Schist", + 0xFFA9BDBF: "Tower Gray", + 0xFFA9BEF2: "Perano", + 0xFFA9C6C2: "Opal", + 0xFFAA375A: "Night Shadz", + 0xFFAA4203: "Fire", + 0xFFAA8B5B: "Muesli", + 0xFFAA8D6F: "Sandal", + 0xFFAAA5A9: "Shady Lady", + 0xFFAAA9CD: "Logan", + 0xFFAAABB7: "Spun Pearl", + 0xFFAAD6E6: "Regent St Blue", + 0xFFAAF0D1: "Magic Mint", + 0xFFAB0563: "Lipstick", + 0xFFAB3472: "Royal Heath", + 0xFFAB917A: "Sandrift", + 0xFFABA0D9: "Cold Purple", + 0xFFABA196: "Bronco", + 0xFFAC8A56: "Limed Oak", + 0xFFAC91CE: "East Side", + 0xFFAC9E22: "Lemon Ginger", + 0xFFACA494: "Napa", + 0xFFACA586: "Hillary", + 0xFFACA59F: "Cloudy", + 0xFFACACAC: "Silver Chalice", + 0xFFACB78E: "Swamp Green", + 0xFFACCBB1: "Spring Rain", + 0xFFACDD4D: "Conifer", + 0xFFACE1AF: "Celadon", + 0xFFAD781B: "Mandalay", + 0xFFADBED1: "Casper", + 0xFFADDFAD: "Moss Green", + 0xFFADE6C4: "Padua", + 0xFFADFF2F: "Green Yellow", + 0xFFAE4560: "Hippie Pink", + 0xFFAE6020: "Desert", + 0xFFAE809E: "Bouquet", + 0xFFAF4035: "Medium Carmine", + 0xFFAF4D43: "Apple Blossom", + 0xFFAF593E: "Brown Rust", + 0xFFAF8751: "Driftwood", + 0xFFAF8F2C: "Alpine", + 0xFFAF9F1C: "Lucky", + 0xFFAFA09E: "Martini", + 0xFFAFB1B8: "Bombay", + 0xFFAFBDD9: "Pigeon Post", + 0xFFB04C6A: "Cadillac", + 0xFFB05D54: "Matrix", + 0xFFB05E81: "Tapestry", + 0xFFB06608: "Mai Tai", + 0xFFB09A95: "Del Rio", + 0xFFB0E0E6: "Powder Blue", + 0xFFB0E313: "Inch Worm", + 0xFFB10000: "Bright Red", + 0xFFB14A0B: "Vesuvius", + 0xFFB1610B: "Pumpkin Skin", + 0xFFB16D52: "Santa Fe", + 0xFFB19461: "Teak", + 0xFFB1E2C1: "Fringy Flower", + 0xFFB1F4E7: "Ice Cold", + 0xFFB20931: "Shiraz", + 0xFFB2A1EA: "Biloba Flower", + 0xFFB32D29: "Tall Poppy", + 0xFFB35213: "Fiery Orange", + 0xFFB38007: "Hot Toddy", + 0xFFB3AF95: "Taupe Gray", + 0xFFB3C110: "La Rioja", + 0xFFB43332: "Well Read", + 0xFFB44668: "Blush", + 0xFFB4CFD3: "Jungle Mist", + 0xFFB57281: "Turkish Rose", + 0xFFB57EDC: "Lavender", + 0xFFB5A27F: "Mongoose", + 0xFFB5B35C: "Olive Green", + 0xFFB5D2CE: "Jet Stream", + 0xFFB5ECDF: "Cruise", + 0xFFB6316C: "Hibiscus", + 0xFFB69D98: "Thatch", + 0xFFB6B095: "Heathered Gray", + 0xFFB6BAA4: "Eagle", + 0xFFB6D1EA: "Spindle", + 0xFFB6D3BF: "Gum Leaf", + 0xFFB7410E: "Rust", + 0xFFB78E5C: "Muddy Waters", + 0xFFB7A214: "Sahara", + 0xFFB7A458: "Husk", + 0xFFB7B1B1: "Nobel", + 0xFFB7C3D0: "Heather", + 0xFFB7F0BE: "Madang", + 0xFFB81104: "Milano Red", + 0xFFB87333: "Copper", + 0xFFB8B56A: "Gimblet", + 0xFFB8C1B1: "Green Spring", + 0xFFB8C25D: "Celery", + 0xFFB8E0F9: "Sail", + 0xFFB94E48: "Chestnut", + 0xFFB95140: "Crail", + 0xFFB98D28: "Marigold", + 0xFFB9C46A: "Wild Willow", + 0xFFB9C8AC: "Rainee", + 0xFFBA0101: "Guardsman Red", + 0xFFBA450C: "Rock Spray", + 0xFFBA6F1E: "Bourbon", + 0xFFBA7F03: "Pirate Gold", + 0xFFBAB1A2: "Nomad", + 0xFFBAC7C9: "Submarine", + 0xFFBAEEF9: "Charlotte", + 0xFFBB3385: "Medium Red Violet", + 0xFFBB8983: "Brandy Rose", + 0xFFBBD009: "Rio Grande", + 0xFFBBD7C1: "Surf", + 0xFFBCC9C2: "Powder Ash", + 0xFFBD5E2E: "Tuscany", + 0xFFBD978E: "Quicksand", + 0xFFBDB1A8: "Silk", + 0xFFBDB2A1: "Malta", + 0xFFBDB3C7: "Chatelle", + 0xFFBDBBD7: "Lavender Gray", + 0xFFBDBDC6: "French Gray", + 0xFFBDC8B3: "Clay Ash", + 0xFFBDC9CE: "Loblolly", + 0xFFBDEDFD: "French Pass", + 0xFFBEA6C3: "London Hue", + 0xFFBEB5B7: "Pink Swan", + 0xFFBEDE0D: "Fuego", + 0xFFBF5500: "Rose of Sharon", + 0xFFBFB8B0: "Tide", + 0xFFBFBED8: "Blue Haze", + 0xFFBFC1C2: "Silver Sand", + 0xFFBFC921: "Key Lime Pie", + 0xFFBFDBE2: "Ziggurat", + 0xFFBFFF00: "Lime", + 0xFFC02B18: "Thunderbird", + 0xFFC04737: "Mojo", + 0xFFC08081: "Old Rose", + 0xFFC0C0C0: "Silver", + 0xFFC0D3B9: "Pale Leaf", + 0xFFC0D8B6: "Pixie Green", + 0xFFC1440E: "Tia Maria", + 0xFFC154C1: "Fuchsia Pink", + 0xFFC1A004: "Buddha Gold", + 0xFFC1B7A4: "Bison Hide", + 0xFFC1BAB0: "Tea", + 0xFFC1BECD: "Gray Suit", + 0xFFC1D7B0: "Sprout", + 0xFFC1F07C: "Sulu", + 0xFFC26B03: "Indochine", + 0xFFC2955D: "Twine", + 0xFFC2BDB6: "Cotton Seed", + 0xFFC2CAC4: "Pumice", + 0xFFC2E8E5: "Jagged Ice", + 0xFFC32148: "Maroon Flush", + 0xFFC3B091: "Indian Khaki", + 0xFFC3BFC1: "Pale Slate", + 0xFFC3C3BD: "Gray Nickel", + 0xFFC3CDE6: "Periwinkle Gray", + 0xFFC3D1D1: "Tiara", + 0xFFC3DDF9: "Tropical Blue", + 0xFFC41E3A: "Cardinal", + 0xFFC45655: "Fuzzy Wuzzy Brown", + 0xFFC45719: "Orange Roughy", + 0xFFC4C4BC: "Mist Gray", + 0xFFC4D0B0: "Coriander", + 0xFFC4F4EB: "Mint Tulip", + 0xFFC54B8C: "Mulberry", + 0xFFC59922: "Nugget", + 0xFFC5994B: "Tussock", + 0xFFC5DBCA: "Sea Mist", + 0xFFC5E17A: "Yellow Green", + 0xFFC62D42: "Brick Red", + 0xFFC6726B: "Contessa", + 0xFFC69191: "Oriental Pink", + 0xFFC6A84B: "Roti", + 0xFFC6C3B5: "Ash", + 0xFFC6C8BD: "Kangaroo", + 0xFFC6E610: "Las Palmas", + 0xFFC7031E: "Monza", + 0xFFC71585: "Red Violet", + 0xFFC7BCA2: "Coral Reef", + 0xFFC7C1FF: "Melrose", + 0xFFC7C4BF: "Cloud", + 0xFFC7C9D5: "Ghost", + 0xFFC7CD90: "Pine Glade", + 0xFFC7DDE5: "Botticelli", + 0xFFC88A65: "Antique Brass", + 0xFFC8A2C8: "Lilac", + 0xFFC8A528: "Hokey Pokey", + 0xFFC8AABF: "Lily", + 0xFFC8B568: "Laser", + 0xFFC8E3D7: "Edgewater", + 0xFFC96323: "Piper", + 0xFFC99415: "Pizza", + 0xFFC9A0DC: "Light Wisteria", + 0xFFC9B29B: "Rodeo Dust", + 0xFFC9B35B: "Sundance", + 0xFFC9B93B: "Earls Green", + 0xFFC9C0BB: "Silver Rust", + 0xFFC9D9D2: "Conch", + 0xFFC9FFA2: "Reef", + 0xFFC9FFE5: "Aero Blue", + 0xFFCA3435: "Flush Mahogany", + 0xFFCABB48: "Turmeric", + 0xFFCADCD4: "Paris White", + 0xFFCAE00D: "Bitter Lemon", + 0xFFCAE6DA: "Skeptic", + 0xFFCB8FA9: "Viola", + 0xFFCBCAB6: "Foggy Gray", + 0xFFCBD3B0: "Green Mist", + 0xFFCBDBD6: "Nebula", + 0xFFCC3333: "Persian Red", + 0xFFCC5500: "Burnt Orange", + 0xFFCC7722: "Ochre", + 0xFFCC8899: "Puce", + 0xFFCCCAA8: "Thistle Green", + 0xFFCCCCFF: "Periwinkle", + 0xFFCCFF00: "Electric Lime", + 0xFFCD5700: "Tenn", + 0xFFCD5C5C: "Chestnut Rose", + 0xFFCD8429: "Brandy Punch", + 0xFFCDF4FF: "Onahau", + 0xFFCEB98F: "Sorrell Brown", + 0xFFCEBABA: "Cold Turkey", + 0xFFCEC291: "Yuma", + 0xFFCEC7A7: "Chino", + 0xFFCFA39D: "Eunry", + 0xFFCFB53B: "Old Gold", + 0xFFCFDCCF: "Tasman", + 0xFFCFE5D2: "Surf Crest", + 0xFFCFF9F3: "Humming Bird", + 0xFFCFFAF4: "Scandal", + 0xFFD05F04: "Red Stage", + 0xFFD06DA1: "Hopbush", + 0xFFD07D12: "Meteor", + 0xFFD0BEF8: "Perfume", + 0xFFD0C0E5: "Prelude", + 0xFFD0F0C0: "Tea Green", + 0xFFD18F1B: "Geebung", + 0xFFD1BEA8: "Vanilla", + 0xFFD1C6B4: "Soft Amber", + 0xFFD1D2CA: "Celeste", + 0xFFD1D2DD: "Mischka", + 0xFFD1E231: "Pear", + 0xFFD2691E: "Hot Cinnamon", + 0xFFD27D46: "Raw Sienna", + 0xFFD29EAA: "Careys Pink", + 0xFFD2B48C: "Tan", + 0xFFD2DA97: "Deco", + 0xFFD2F6DE: "Blue Romance", + 0xFFD2F8B0: "Gossip", + 0xFFD3CBBA: "Sisal", + 0xFFD3CDC5: "Swirl", + 0xFFD47494: "Charm", + 0xFFD4B6AF: "Clam Shell", + 0xFFD4BF8D: "Straw", + 0xFFD4C4A8: "Akaroa", + 0xFFD4CD16: "Bird Flower", + 0xFFD4D7D9: "Iron", + 0xFFD4DFE2: "Geyser", + 0xFFD4E2FC: "Hawkes Blue", + 0xFFD54600: "Grenadier", + 0xFFD591A4: "Can Can", + 0xFFD59A6F: "Whiskey", + 0xFFD5D195: "Winter Hazel", + 0xFFD5F6E3: "Granny Apple", + 0xFFD69188: "My Pink", + 0xFFD6C562: "Tacha", + 0xFFD6CEF6: "Moon Raker", + 0xFFD6D6D1: "Quill Gray", + 0xFFD6FFDB: "Snowy Mint", + 0xFFD7837F: "New York Pink", + 0xFFD7C498: "Pavlova", + 0xFFD7D0FF: "Fog", + 0xFFD84437: "Valencia", + 0xFFD87C63: "Japonica", + 0xFFD8BFD8: "Thistle", + 0xFFD8C2D5: "Maverick", + 0xFFD8FCFA: "Foam", + 0xFFD94972: "Cabaret", + 0xFFD99376: "Burning Sand", + 0xFFD9B99B: "Cameo", + 0xFFD9D6CF: "Timberwolf", + 0xFFD9DCC1: "Tana", + 0xFFD9E4F5: "Link Water", + 0xFFD9F7FF: "Mabel", + 0xFFDA3287: "Cerise", + 0xFFDA5B38: "Flame Pea", + 0xFFDA6304: "Bamboo", + 0xFFDA6A41: "Red Damask", + 0xFFDA70D6: "Orchid", + 0xFFDA8A67: "Copperfield", + 0xFFDAA520: "Golden Grass", + 0xFFDAECD6: "Zanah", + 0xFFDAF4F0: "Iceberg", + 0xFFDAFAFF: "Oyster Bay", + 0xFFDB5079: "Cranberry", + 0xFFDB9690: "Petite Orchid", + 0xFFDB995E: "Di Serria", + 0xFFDBDBDB: "Alto", + 0xFFDBFFF8: "Frosted Mint", + 0xFFDC143C: "Crimson", + 0xFFDC4333: "Punch", + 0xFFDCB20C: "Galliano", + 0xFFDCB4BC: "Blossom", + 0xFFDCD747: "Wattle", + 0xFFDCD9D2: "Westar", + 0xFFDCDDCC: "Moon Mist", + 0xFFDCEDB4: "Caper", + 0xFFDCF0EA: "Swans Down", + 0xFFDDD6D5: "Swiss Coffee", + 0xFFDDF9F1: "White Ice", + 0xFFDE3163: "Cerise Red", + 0xFFDE6360: "Roman", + 0xFFDEA681: "Tumbleweed", + 0xFFDEBA13: "Gold Tips", + 0xFFDEC196: "Brandy", + 0xFFDECBC6: "Wafer", + 0xFFDED4A4: "Sapling", + 0xFFDED717: "Barberry", + 0xFFDEE5C0: "Beryl Green", + 0xFFDEF5FF: "Pattens Blue", + 0xFFDF73FF: "Heliotrope", + 0xFFDFBE6F: "Apache", + 0xFFDFCD6F: "Chenin", + 0xFFDFCFDB: "Lola", + 0xFFDFECDA: "Willow Brook", + 0xFFDFFF00: "Chartreuse Yellow", + 0xFFE0B0FF: "Mauve", + 0xFFE0B646: "Anzac", + 0xFFE0B974: "Harvest Gold", + 0xFFE0C095: "Calico", + 0xFFE0FFFF: "Baby Blue", + 0xFFE16865: "Sunglo", + 0xFFE1BC64: "Equator", + 0xFFE1C0C8: "Pink Flare", + 0xFFE1E6D6: "Periglacial Blue", + 0xFFE1EAD4: "Kidnapper", + 0xFFE1F6E8: "Tara", + 0xFFE25465: "Mandy", + 0xFFE2725B: "Terracotta", + 0xFFE28913: "Golden Bell", + 0xFFE292C0: "Shocking", + 0xFFE29418: "Dixie", + 0xFFE29CD2: "Light Orchid", + 0xFFE2D8ED: "Snuff", + 0xFFE2EBED: "Mystic", + 0xFFE2F3EC: "Apple Green", + 0xFFE30B5C: "Razzmatazz", + 0xFFE32636: "Alizarin Crimson", + 0xFFE34234: "Cinnabar", + 0xFFE3BEBE: "Cavern Pink", + 0xFFE3F5E1: "Peppermint", + 0xFFE3F988: "Mindaro", + 0xFFE47698: "Deep Blush", + 0xFFE49B0F: "Gamboge", + 0xFFE4C2D5: "Melanie", + 0xFFE4CFDE: "Twilight", + 0xFFE4D1C0: "Bone", + 0xFFE4D422: "Sunflower", + 0xFFE4D5B7: "Grain Brown", + 0xFFE4D69B: "Zombie", + 0xFFE4F6E7: "Frostee", + 0xFFE4FFD1: "Snow Flurry", + 0xFFE52B50: "Amaranth", + 0xFFE5841B: "Zest", + 0xFFE5CCC9: "Dust Storm", + 0xFFE5D7BD: "Stark White", + 0xFFE5D8AF: "Hampton", + 0xFFE5E0E1: "Bon Jour", + 0xFFE5E5E5: "Mercury", + 0xFFE5F9F6: "Polar", + 0xFFE64E03: "Trinidad", + 0xFFE6BE8A: "Gold Sand", + 0xFFE6BEA5: "Cashmere", + 0xFFE6D7B9: "Double Spanish White", + 0xFFE6E4D4: "Satin Linen", + 0xFFE6F2EA: "Harp", + 0xFFE6F8F3: "Off Green", + 0xFFE6FFE9: "Hint of Green", + 0xFFE6FFFF: "Tranquil", + 0xFFE77200: "Mango Tango", + 0xFFE7730A: "Christine", + 0xFFE79F8C: "Tonys Pink", + 0xFFE79FC4: "Kobi", + 0xFFE7BCB4: "Rose Fog", + 0xFFE7BF05: "Corn", + 0xFFE7CD8C: "Putty", + 0xFFE7ECE6: "Gray Nurse", + 0xFFE7F8FF: "Lily White", + 0xFFE7FEFF: "Bubbles", + 0xFFE89928: "Fire Bush", + 0xFFE8B9B3: "Shilo", + 0xFFE8E0D5: "Pearl Bush", + 0xFFE8EBE0: "Green White", + 0xFFE8F1D4: "Chrome White", + 0xFFE8F2EB: "Gin", + 0xFFE8F5F2: "Aqua Squeeze", + 0xFFE96E00: "Clementine", + 0xFFE97451: "Burnt Sienna", + 0xFFE97C07: "Tahiti Gold", + 0xFFE9CECD: "Oyster Pink", + 0xFFE9D75A: "Confetti", + 0xFFE9E3E3: "Ebb", + 0xFFE9F8ED: "Ottoman", + 0xFFE9FFFD: "Clear Day", + 0xFFEA88A8: "Carissma", + 0xFFEAAE69: "Porsche", + 0xFFEAB33B: "Tulip Tree", + 0xFFEAC674: "Rob Roy", + 0xFFEADAB8: "Raffia", + 0xFFEAE8D4: "White Rock", + 0xFFEAF6EE: "Panache", + 0xFFEAF6FF: "Solitude", + 0xFFEAF9F5: "Aqua Spring", + 0xFFEAFFFE: "Dew", + 0xFFEB9373: "Apricot", + 0xFFEBC2AF: "Zinnwaldite", + 0xFFECA927: "Fuel Yellow", + 0xFFECC54E: "Ronchi", + 0xFFECC7EE: "French Lilac", + 0xFFECCDB9: "Just Right", + 0xFFECE090: "Wild Rice", + 0xFFECEBBD: "Fall Green", + 0xFFECEBCE: "Aths Special", + 0xFFECF245: "Starship", + 0xFFED0A3F: "Red Ribbon", + 0xFFED7A1C: "Tango", + 0xFFED9121: "Carrot Orange", + 0xFFED989E: "Sea Pink", + 0xFFEDB381: "Tacao", + 0xFFEDC9AF: "Desert Sand", + 0xFFEDCDAB: "Pancho", + 0xFFEDDCB1: "Chamois", + 0xFFEDEA99: "Primrose", + 0xFFEDF5DD: "Frost", + 0xFFEDF5F5: "Aqua Haze", + 0xFFEDF6FF: "Zumthor", + 0xFFEDF9F1: "Narvik", + 0xFFEDFC84: "Honeysuckle", + 0xFFEE82EE: "Lavender Magenta", + 0xFFEEC1BE: "Beauty Bush", + 0xFFEED794: "Chalky", + 0xFFEED9C4: "Almond", + 0xFFEEDC82: "Flax", + 0xFFEEDEDA: "Bizarre", + 0xFFEEE3AD: "Double Colonial White", + 0xFFEEEEE8: "Cararra", + 0xFFEEEF78: "Manz", + 0xFFEEF0C8: "Tahuna Sands", + 0xFFEEF0F3: "Athens Gray", + 0xFFEEF3C3: "Tusk", + 0xFFEEF4DE: "Loafer", + 0xFFEEF6F7: "Catskill White", + 0xFFEEFDFF: "Twilight Blue", + 0xFFEEFF9A: "Jonquil", + 0xFFEEFFE2: "Rice Flower", + 0xFFEF863F: "Jaffa", + 0xFFEFEFEF: "Gallery", + 0xFFEFF2F3: "Porcelain", + 0xFFF091A9: "Mauvelous", + 0xFFF0D52D: "Golden Dream", + 0xFFF0DB7D: "Golden Sand", + 0xFFF0DC82: "Buff", + 0xFFF0E2EC: "Prim", + 0xFFF0E68C: "Khaki", + 0xFFF0EEFD: "Selago", + 0xFFF0EEFF: "Titan White", + 0xFFF0F8FF: "Alice Blue", + 0xFFF0FCEA: "Feta", + 0xFFF18200: "Gold Drop", + 0xFFF19BAB: "Wewak", + 0xFFF1E788: "Sahara Sand", + 0xFFF1E9D2: "Parchment", + 0xFFF1E9FF: "Blue Chalk", + 0xFFF1EEC1: "Mint Julep", + 0xFFF1F1F1: "Seashell", + 0xFFF1F7F2: "Saltpan", + 0xFFF1FFAD: "Tidal", + 0xFFF1FFC8: "Chiffon", + 0xFFF2552A: "Flamingo", + 0xFFF28500: "Tangerine", + 0xFFF2C3B2: "Mandys Pink", + 0xFFF2F2F2: "Concrete", + 0xFFF2FAFA: "Black Squeeze", + 0xFFF34723: "Pomegranate", + 0xFFF3AD16: "Buttercup", + 0xFFF3D69D: "New Orleans", + 0xFFF3D9DF: "Vanilla Ice", + 0xFFF3E7BB: "Sidecar", + 0xFFF3E9E5: "Dawn Pink", + 0xFFF3EDCF: "Wheatfield", + 0xFFF3FB62: "Canary", + 0xFFF3FBD4: "Orinoco", + 0xFFF3FFD8: "Carla", + 0xFFF400A1: "Hollywood Cerise", + 0xFFF4A460: "Sandy brown", + 0xFFF4C430: "Saffron", + 0xFFF4D81C: "Ripe Lemon", + 0xFFF4EBD3: "Janna", + 0xFFF4F2EE: "Pampas", + 0xFFF4F4F4: "Wild Sand", + 0xFFF4F8FF: "Zircon", + 0xFFF57584: "Froly", + 0xFFF5C85C: "Cream Can", + 0xFFF5C999: "Manhattan", + 0xFFF5D5A0: "Maize", + 0xFFF5DEB3: "Wheat", + 0xFFF5E7A2: "Sandwisp", + 0xFFF5E7E2: "Pot Pourri", + 0xFFF5E9D3: "Albescent White", + 0xFFF5EDEF: "Soft Peach", + 0xFFF5F3E5: "Ecru White", + 0xFFF5F5DC: "Beige", + 0xFFF5FB3D: "Golden Fizz", + 0xFFF5FFBE: "Australian Mint", + 0xFFF64A8A: "French Rose", + 0xFFF653A6: "Brilliant Rose", + 0xFFF6A4C9: "Illusion", + 0xFFF6F0E6: "Merino", + 0xFFF6F7F7: "Black Haze", + 0xFFF6FFDC: "Spring Sun", + 0xFFF7468A: "Violet Red", + 0xFFF77703: "Chilean Fire", + 0xFFF77FBE: "Persian Pink", + 0xFFF7B668: "Rajah", + 0xFFF7C8DA: "Azalea", + 0xFFF7DBE6: "We Peep", + 0xFFF7F2E1: "Quarter Spanish White", + 0xFFF7F5FA: "Whisper", + 0xFFF7FAF7: "Snow Drift", + 0xFFF8B853: "Casablanca", + 0xFFF8C3DF: "Chantilly", + 0xFFF8D9E9: "Cherub", + 0xFFF8DB9D: "Marzipan", + 0xFFF8DD5C: "Energy Yellow", + 0xFFF8E4BF: "Givry", + 0xFFF8F0E8: "White Linen", + 0xFFF8F4FF: "Magnolia", + 0xFFF8F6F1: "Spring Wood", + 0xFFF8F7DC: "Coconut Cream", + 0xFFF8F7FC: "White Lilac", + 0xFFF8F8F7: "Desert Storm", + 0xFFF8F99C: "Texas", + 0xFFF8FACD: "Corn Field", + 0xFFF8FDD3: "Mimosa", + 0xFFF95A61: "Carnation", + 0xFFF9BF58: "Saffron Mango", + 0xFFF9E0ED: "Carousel Pink", + 0xFFF9E4BC: "Dairy Cream", + 0xFFF9E663: "Portica", + 0xFFF9EAF3: "Amour", + 0xFFF9F8E4: "Rum Swizzle", + 0xFFF9FF8B: "Dolly", + 0xFFF9FFF6: "Sugar Cane", + 0xFFFA7814: "Ecstasy", + 0xFFFA9D5A: "Tan Hide", + 0xFFFAD3A2: "Corvette", + 0xFFFADFAD: "Peach Yellow", + 0xFFFAE600: "Turbo", + 0xFFFAEAB9: "Astra", + 0xFFFAECCC: "Champagne", + 0xFFFAF0E6: "Linen", + 0xFFFAF3F0: "Fantasy", + 0xFFFAF7D6: "Citrine White", + 0xFFFAFAFA: "Alabaster", + 0xFFFAFDE4: "Hint of Yellow", + 0xFFFAFFA4: "Milan", + 0xFFFB607F: "Brink Pink", + 0xFFFB8989: "Geraldine", + 0xFFFBA0E3: "Lavender Rose", + 0xFFFBA129: "Sea Buckthorn", + 0xFFFBAC13: "Sun", + 0xFFFBAED2: "Lavender Pink", + 0xFFFBB2A3: "Rose Bud", + 0xFFFBBEDA: "Cupid", + 0xFFFBCCE7: "Classic Rose", + 0xFFFBCEB1: "Apricot Peach", + 0xFFFBE7B2: "Banana Mania", + 0xFFFBE870: "Marigold Yellow", + 0xFFFBE96C: "Festival", + 0xFFFBEA8C: "Sweet Corn", + 0xFFFBEC5D: "Candy Corn", + 0xFFFBF9F9: "Hint of Red", + 0xFFFBFFBA: "Shalimar", + 0xFFFC0FC0: "Shocking Pink", + 0xFFFC80A5: "Tickle Me Pink", + 0xFFFC9C1D: "Tree Poppy", + 0xFFFCC01E: "Lightning Yellow", + 0xFFFCD667: "Goldenrod", + 0xFFFCD917: "Candlelight", + 0xFFFCDA98: "Cherokee", + 0xFFFCF4D0: "Double Pearl Lusta", + 0xFFFCF4DC: "Pearl Lusta", + 0xFFFCF8F7: "Vista White", + 0xFFFCFBF3: "Bianca", + 0xFFFCFEDA: "Moon Glow", + 0xFFFCFFE7: "China Ivory", + 0xFFFCFFF9: "Ceramic", + 0xFFFD0E35: "Torch Red", + 0xFFFD5B78: "Wild Watermelon", + 0xFFFD7B33: "Crusta", + 0xFFFD7C07: "Sorbus", + 0xFFFD9FA2: "Sweet Pink", + 0xFFFDD5B1: "Light Apricot", + 0xFFFDD7E4: "Pig Pink", + 0xFFFDE1DC: "Cinderella", + 0xFFFDE295: "Golden Glow", + 0xFFFDE910: "Lemon", + 0xFFFDF5E6: "Old Lace", + 0xFFFDF6D3: "Half Colonial White", + 0xFFFDF7AD: "Drover", + 0xFFFDFEB8: "Pale Prim", + 0xFFFDFFD5: "Cumulus", + 0xFFFE28A2: "Persian Rose", + 0xFFFE4C40: "Sunset Orange", + 0xFFFE6F5E: "Bittersweet", + 0xFFFE9D04: "California", + 0xFFFEA904: "Yellow Sea", + 0xFFFEBAAD: "Melon", + 0xFFFED33C: "Bright Sun", + 0xFFFED85D: "Dandelion", + 0xFFFEDB8D: "Salomie", + 0xFFFEE5AC: "Cape Honey", + 0xFFFEEBF3: "Remy", + 0xFFFEEFCE: "Oasis", + 0xFFFEF0EC: "Bridesmaid", + 0xFFFEF2C7: "Beeswax", + 0xFFFEF3D8: "Bleach White", + 0xFFFEF4CC: "Pipi", + 0xFFFEF4DB: "Half Spanish White", + 0xFFFEF4F8: "Wisp Pink", + 0xFFFEF5F1: "Provincial Pink", + 0xFFFEF7DE: "Half Dutch White", + 0xFFFEF8E2: "Solitaire", + 0xFFFEF8FF: "White Pointer", + 0xFFFEF9E3: "Off Yellow", + 0xFFFEFCED: "Orange White", + 0xFFFF0000: "Red", + 0xFFFF007F: "Rose", + 0xFFFF00CC: "Purple Pizzazz", + 0xFFFF00FF: "Magenta / Fuchsia", + 0xFFFF2400: "Scarlet", + 0xFFFF3399: "Wild Strawberry", + 0xFFFF33CC: "Razzle Dazzle Rose", + 0xFFFF355E: "Radical Red", + 0xFFFF3F34: "Red Orange", + 0xFFFF4040: "Coral Red", + 0xFFFF4D00: "Vermilion", + 0xFFFF4F00: "International Orange", + 0xFFFF6037: "Outrageous Orange", + 0xFFFF6600: "Blaze Orange", + 0xFFFF66FF: "Pink Flamingo", + 0xFFFF681F: "Orange", + 0xFFFF69B4: "Hot Pink", + 0xFFFF6B53: "Persimmon", + 0xFFFF6FFF: "Blush Pink", + 0xFFFF7034: "Burning Orange", + 0xFFFF7518: "Pumpkin", + 0xFFFF7D07: "Flamenco", + 0xFFFF7F00: "Flush Orange", + 0xFFFF7F50: "Coral", + 0xFFFF8C69: "Salmon", + 0xFFFF9000: "Pizazz", + 0xFFFF910F: "West Side", + 0xFFFF91A4: "Pink Salmon", + 0xFFFF9933: "Neon Carrot", + 0xFFFF9966: "Atomic Tangerine", + 0xFFFF9980: "Vivid Tangerine", + 0xFFFF9E2C: "Sunshade", + 0xFFFFA000: "Orange Peel", + 0xFFFFA194: "Mona Lisa", + 0xFFFFA500: "Web Orange", + 0xFFFFA6C9: "Carnation Pink", + 0xFFFFAB81: "Hit Pink", + 0xFFFFAE42: "Yellow Orange", + 0xFFFFB0AC: "Cornflower Lilac", + 0xFFFFB1B3: "Sundown", + 0xFFFFB31F: "My Sin", + 0xFFFFB555: "Texas Rose", + 0xFFFFB7D5: "Cotton Candy", + 0xFFFFB97B: "Macaroni and Cheese", + 0xFFFFBA00: "Selective Yellow", + 0xFFFFBD5F: "Koromiko", + 0xFFFFBF00: "Amber", + 0xFFFFC0A8: "Wax Flower", + 0xFFFFC0CB: "Pink", + 0xFFFFC3C0: "Your Pink", + 0xFFFFC901: "Supernova", + 0xFFFFCBA4: "Flesh", + 0xFFFFCC33: "Sunglow", + 0xFFFFCC5C: "Golden Tainoi", + 0xFFFFCC99: "Peach Orange", + 0xFFFFCD8C: "Chardonnay", + 0xFFFFD1DC: "Pastel Pink", + 0xFFFFD2B7: "Romantic", + 0xFFFFD38C: "Grandis", + 0xFFFFD700: "Gold", + 0xFFFFD800: "School bus Yellow", + 0xFFFFD8D9: "Cosmos", + 0xFFFFDB58: "Mustard", + 0xFFFFDCD6: "Peach Schnapps", + 0xFFFFDDAF: "Caramel", + 0xFFFFDDCD: "Tuft Bush", + 0xFFFFDDCF: "Watusi", + 0xFFFFDDF4: "Pink Lace", + 0xFFFFDEAD: "Navajo White", + 0xFFFFDEB3: "Frangipani", + 0xFFFFE1DF: "Pippin", + 0xFFFFE1F2: "Pale Rose", + 0xFFFFE2C5: "Negroni", + 0xFFFFE5A0: "Cream Brulee", + 0xFFFFE5B4: "Peach", + 0xFFFFE6C7: "Tequila", + 0xFFFFE772: "Kournikova", + 0xFFFFEAC8: "Sandy Beach", + 0xFFFFEAD4: "Karry", + 0xFFFFEC13: "Broom", + 0xFFFFEDBC: "Colonial White", + 0xFFFFEED8: "Derby", + 0xFFFFEFA1: "Vis Vis", + 0xFFFFEFC1: "Egg White", + 0xFFFFEFD5: "Papaya Whip", + 0xFFFFEFEC: "Fair Pink", + 0xFFFFF0DB: "Peach Cream", + 0xFFFFF0F5: "Lavender blush", + 0xFFFFF14F: "Gorse", + 0xFFFFF1B5: "Buttermilk", + 0xFFFFF1D8: "Pink Lady", + 0xFFFFF1EE: "Forget Me Not", + 0xFFFFF1F9: "Tutu", + 0xFFFFF39D: "Picasso", + 0xFFFFF3F1: "Chardon", + 0xFFFFF46E: "Paris Daisy", + 0xFFFFF4CE: "Barley White", + 0xFFFFF4DD: "Egg Sour", + 0xFFFFF4E0: "Sazerac", + 0xFFFFF4E8: "Serenade", + 0xFFFFF4F3: "Chablis", + 0xFFFFF5EE: "Seashell Peach", + 0xFFFFF5F3: "Sauvignon", + 0xFFFFF6D4: "Milk Punch", + 0xFFFFF6DF: "Varden", + 0xFFFFF6F5: "Rose White", + 0xFFFFF8D1: "Baja White", + 0xFFFFF9E2: "Gin Fizz", + 0xFFFFF9E6: "Early Dawn", + 0xFFFFFACD: "Lemon Chiffon", + 0xFFFFFAF4: "Bridal Heath", + 0xFFFFFBDC: "Scotch Mist", + 0xFFFFFBF9: "Soapstone", + 0xFFFFFC99: "Witch Haze", + 0xFFFFFCEA: "Buttery White", + 0xFFFFFCEE: "Island Spice", + 0xFFFFFDD0: "Cream", + 0xFFFFFDE6: "Chilean Heath", + 0xFFFFFDE8: "Travertine", + 0xFFFFFDF3: "Orchid White", + 0xFFFFFDF4: "Quarter Pearl Lusta", + 0xFFFFFEE1: "Half and Half", + 0xFFFFFEEC: "Apricot White", + 0xFFFFFEF0: "Rice Cake", + 0xFFFFFEF6: "Black White", + 0xFFFFFEFD: "Romance", + 0xFFFFFF00: "Yellow", + 0xFFFFFF66: "Laser Lemon", + 0xFFFFFF99: "Pale Canary", + 0xFFFFFFB4: "Portafino", + 0xFFFFFFF0: "Ivory", + 0xFFFFFFFF: "White", + }; +} diff --git a/lib/src/controls/form/color_picker/color_picker.dart b/lib/src/controls/form/color_picker/color_picker.dart new file mode 100644 index 000000000..d2a63027c --- /dev/null +++ b/lib/src/controls/form/color_picker/color_picker.dart @@ -0,0 +1,1143 @@ +import 'dart:math' as math; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'color_spectrum.dart'; +import 'color_state.dart'; + +/// Defines the shape of the color spectrum in the [ColorPicker]. +enum ColorSpectrumShape { + /// A ring-shaped color spectrum. + ring, + + /// A box-shaped color spectrum. + box, +} + +/// Defines the color mode used in the [ColorPicker]. +enum ColorMode { + /// RGB (Red, Green, Blue) color mode. + rgb, + + /// HSV (Hue, Saturation, Value) color mode. + hsv, +} + +/// Color picker spacing constants +class ColorPickerSpacing { + /// Small spacing between widgets + static const double small = 12.0; + + /// Large spacing between widgets + static const double large = 24.0; +} + +/// Color picker component sizing constants +class ColorPickerSizes { + /// The size of the color spectrum widget + static const double spectrum = 336.0; + + /// The width of the color preview box + static const double preview = 44.0; + + /// The height of the sliders + static const double slider = 12.0; + + /// The width of the input boxes + static const double inputBox = 120.0; + + /// The maximum width of the color spectrum and preview box + static double maxWidth = spectrum + ColorPickerSpacing.small + preview; // 392 +} + +/// Color Picker +/// +/// A comprehensive color picker implementation that supports both RGB and HSV color models, +/// with wheel and box spectrum shapes. Integrates with Fluent UI's theming system for +/// consistent look and feel. +/// +/// Features: +/// - Color wheel and box spectrum modes +/// - RGB and HSV color input modes +/// - Alpha channel support +/// - Hex color input +/// - Real-time color name display +/// - Theme-aware tooltips and UI elements +/// - Value and alpha sliders +/// +/// Example usage: +/// ```dart +/// ColorPicker( +/// value: Colors.blue, +/// onChanged: (Color color) { +/// setState(() { +/// _selectedColor = color; +/// }); +/// }, +/// colorSpectrumShape: ColorSpectrumShape.ring, +/// ) +/// ``` +class ColorPicker extends StatefulWidget { + /// The current color value + final Color color; + + /// Callback when the color value changes + final ValueChanged onChanged; + + /// The orientation of the color picker layout + final Axis orientation; + + /// Whether the color preview is visible + final bool isColorPreviewVisible; + + /// Whether the "More" button is visible + final bool isMoreButtonVisible; + + /// Whether the color slider is visible + final bool isColorSliderVisible; + + /// Whether the color channel text input is visible + final bool isColorChannelTextInputVisible; + + /// Whether the hex input is visible + final bool isHexInputVisible; + + /// Whether the alpha channel is enabled + final bool isAlphaEnabled; + + /// Whether the alpha slider is visible + final bool isAlphaSliderVisible; + + /// Whether the alpha text input is visible + final bool isAlphaTextInputVisible; + + /// The shape of the color spectrum (ring or box) + final ColorSpectrumShape colorSpectrumShape; + + /// The minimum allowed hue value (0-359) + final int minHue; + + /// The maximum allowed hue value (0-359) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// The minimum allowed value/brightness (0-100) + final int minValue; + + /// The maximum allowed value/brightness (0-100) + final int maxValue; + + /// Creates a new instance of [ColorPicker]. + /// + /// - [color]: The current color value. + /// - [onChanged]: Callback when the color value changes. + /// - [orientation]: The orientation of the color picker layout. Defaults to [Axis.vertical]. + /// - [colorSpectrumShape]: The shape of the color spectrum (ring or box). Defaults to [ColorSpectrumShape.ring]. + /// - [isColorPreviewVisible]: Whether the color preview is visible. Defaults to true. + /// - [isColorSliderVisible]: Whether the color slider is visible. Defaults to true. + /// - [isMoreButtonVisible]: Whether the "More" button is visible. Defaults to true. + /// - [isHexInputVisible]: Whether the hex input is visible. Defaults to true. + /// - [isColorChannelTextInputVisible]: Whether the color channel text input is visible. Defaults to true. + /// - [isAlphaEnabled]: Whether the alpha channel is enabled. Defaults to true. + /// - [isAlphaSliderVisible]: Whether the alpha slider is visible. Defaults to true. + /// - [isAlphaTextInputVisible]: Whether the alpha text input is visible. Defaults to true. + /// - [minHue]: The minimum allowed hue value (0-359). Defaults to 0. + /// - [maxHue]: The maximum allowed hue value (0-359). Defaults to 359. + /// - [minSaturation]: The minimum allowed saturation value (0-100). Defaults to 0. + /// - [maxSaturation]: The maximum allowed saturation value (0-100). Defaults to 100. + /// - [minValue]: The minimum allowed value/brightness (0-100). Defaults to 0. + /// - [maxValue]: The maximum allowed value/brightness (0-100). Defaults to 100. + const ColorPicker({ + super.key, + required this.color, + required this.onChanged, + this.orientation = Axis.vertical, + this.colorSpectrumShape = ColorSpectrumShape.ring, + this.isColorPreviewVisible = true, + this.isColorSliderVisible = true, + this.isMoreButtonVisible = true, + this.isHexInputVisible = true, + this.isColorChannelTextInputVisible = true, + this.isAlphaEnabled = true, + this.isAlphaSliderVisible = true, + this.isAlphaTextInputVisible = true, + this.minHue = 0, + this.maxHue = 359, + this.minSaturation = 0, + this.maxSaturation = 100, + this.minValue = 0, + this.maxValue = 100, + }) : assert(minHue >= 0 && minHue <= maxHue && maxHue <= 359, + 'Hue values must be between 0 and 359'), + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'), + assert(minValue >= 0 && minValue <= maxValue && maxValue <= 100, + 'Value/brightness values must be between 0 and 100'); + + @override + State createState() => _ColorPickerState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('color', color)); + properties.add(EnumProperty('orientation', orientation)); + properties.add(EnumProperty( + 'colorSpectrumShape', colorSpectrumShape)); + properties.add(FlagProperty( + 'isColorPreviewVisible', + value: isColorPreviewVisible, + defaultValue: true, + ifFalse: 'color preview hidden', + )); + properties.add(FlagProperty( + 'isColorSliderVisible', + value: isColorSliderVisible, + defaultValue: true, + ifFalse: 'color slider hidden', + )); + properties.add(FlagProperty( + 'isMoreButtonVisible', + value: isMoreButtonVisible, + defaultValue: true, + ifFalse: 'more button hidden', + )); + properties.add(FlagProperty( + 'isHexInputVisible', + value: isHexInputVisible, + defaultValue: true, + ifFalse: 'hex input hidden', + )); + properties.add(FlagProperty( + 'isColorChannelTextInputVisible', + value: isColorChannelTextInputVisible, + defaultValue: true, + ifFalse: 'color channel text input hidden', + )); + properties.add(FlagProperty( + 'isAlphaEnabled', + value: isAlphaEnabled, + defaultValue: true, + ifFalse: 'alpha disabled', + )); + properties.add(FlagProperty( + 'isAlphaSliderVisible', + value: isAlphaSliderVisible, + defaultValue: true, + ifFalse: 'alpha slider hidden', + )); + properties.add(FlagProperty( + 'isAlphaTextInputVisible', + value: isAlphaTextInputVisible, + defaultValue: true, + ifFalse: 'alpha text input hidden', + )); + properties.add(IntProperty('minHue', minHue, defaultValue: 0)); + properties.add(IntProperty('maxHue', maxHue, defaultValue: 359)); + properties + .add(IntProperty('minSaturation', minSaturation, defaultValue: 0)); + properties + .add(IntProperty('maxSaturation', maxSaturation, defaultValue: 100)); + properties.add(IntProperty('minValue', minValue, defaultValue: 0)); + properties.add(IntProperty('maxValue', maxValue, defaultValue: 100)); + } +} + +class _ColorPickerState extends State { + late TextEditingController _hexController; + late FocusNode _hexFocusNode; + + late ColorState _colorState; + + bool _isMoreExpanded = false; + + @override + void initState() { + super.initState(); + _colorState = ColorState.fromColor(widget.color); + _colorState.clampToBounds( + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + minValue: widget.minValue, + maxValue: widget.maxValue, + ); + _initControllers(); + _updateControllers(); + } + + @override + void didUpdateWidget(ColorPicker oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.color != widget.color) { + _updateControllers(); + } + } + + @override + void dispose() { + _hexFocusNode.removeListener(_onFocusChange); + _hexFocusNode.dispose(); + _hexController.dispose(); + _colorState.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool hasVisibleInputs = widget.isHexInputVisible || + widget.isColorChannelTextInputVisible || + (widget.isAlphaEnabled && widget.isAlphaTextInputVisible); + + // Build the color picker layout based on orientation + return widget.orientation == Axis.vertical + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSpectrumAndPreview(), + if (widget.isColorSliderVisible || + (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ + const SizedBox(height: ColorPickerSpacing.large), + _buildSliders(), + ], + if (widget.isMoreButtonVisible && hasVisibleInputs) ...[ + const SizedBox(height: ColorPickerSpacing.large), + _buildMoreButton(), + ], + if (!widget.isMoreButtonVisible || _isMoreExpanded) ...[ + if (hasVisibleInputs) ...[ + const SizedBox(height: ColorPickerSpacing.large), + _buildInputs(), + ], + ], + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSpectrumAndPreview(), + if (widget.isColorSliderVisible || + (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ + const SizedBox(width: ColorPickerSpacing.large), + _buildSliders(), + ], + if (hasVisibleInputs) ...[ + const SizedBox(width: ColorPickerSpacing.large), + _buildInputs(), + ], + ], + ); + } + + /// Initializes the text controllers and focus nodes. + void _initControllers() { + _hexController = TextEditingController(); + _hexFocusNode = FocusNode(); + _hexFocusNode.addListener(_onFocusChange); + } + + /// Updates the text controllers with the current color state. + void _updateControllers() { + final oldText = _hexController.text; + final newText = _colorState.toHexString(widget.isAlphaEnabled); + if (oldText != newText) { + _hexController.text = newText; + } + } + + /// Builds the color spectrum and color preview box. + Widget _buildSpectrumAndPreview() { + return _ColorSpectrumAndPreview( + colorState: _colorState, + orientation: widget.orientation, + colorSpectrumShape: widget.colorSpectrumShape, + isColorPreviewVisible: widget.isColorPreviewVisible, + onColorChanged: _handleColorChanged, + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + ); + } + + /// Builds the color sliders. + Widget _buildSliders() { + return _ColorSliders( + colorState: _colorState, + orientation: widget.orientation, + isColorSliderVisible: widget.isColorSliderVisible, + isAlphaSliderVisible: widget.isAlphaSliderVisible, + isAlphaEnabled: widget.isAlphaEnabled, + onColorChanged: _handleColorChanged, + minValue: widget.minValue, + maxValue: widget.maxValue, + ); + } + + /// Builds the "More" button to expand the color picker inputs. + Widget _buildMoreButton() { + final moreButton = SizedBox( + width: ColorPickerSizes.inputBox, + child: Button( + onPressed: () => setState(() => _isMoreExpanded = !_isMoreExpanded), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(_isMoreExpanded ? 'Less' : 'More'), + Icon( + _isMoreExpanded + ? FluentIcons.chevron_up + : FluentIcons.chevron_down, + size: 12, + ), + ], + ), + )); + + return SizedBox( + width: ColorPickerSizes.maxWidth, + child: Align( + alignment: Alignment.centerRight, + child: moreButton, + ), + ); + } + + /// Builds the color inputs. + Widget _buildInputs() { + return _ColorInputs( + colorState: _colorState, + orientation: widget.orientation, + isMoreExpanded: _isMoreExpanded, + isMoreButtonVisible: widget.isMoreButtonVisible, + isHexInputVisible: widget.isHexInputVisible, + isColorChannelTextInputVisible: widget.isColorChannelTextInputVisible, + isAlphaEnabled: widget.isAlphaEnabled, + isAlphaTextInputVisible: widget.isAlphaTextInputVisible, + hexController: _hexController, + onColorChanged: _handleColorChanged, + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + minValue: widget.minValue, + maxValue: widget.maxValue, + ); + } + + /// Handles color changes from the color spectrum, sliders, and inputs. + void _handleColorChanged(ColorState newState) { + newState.clampToBounds( + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + minValue: widget.minValue, + maxValue: widget.maxValue, + ); + + setState(() => _colorState = newState); + widget.onChanged(newState.toColor()); + } + + /// Callback when the hex input field loses focus. + void _onFocusChange() { + if (!_hexFocusNode.hasFocus) { + _updateHexColor(_hexController.text); + } + } + + /// Updates the color state based on the hex color value. + void _updateHexColor(String text) { + if (text.length == 7 || (widget.isAlphaEnabled && text.length == 9)) { + try { + _colorState.setHex(text); + widget.onChanged(_colorState.toColor()); + } catch (_) { + _updateControllers(); + } + } + } +} + +/// A widget that displays the color spectrum and color preview box. +class _ColorSpectrumAndPreview extends StatelessWidget { + /// The current color state + final ColorState colorState; + + /// Callback when the color changes + final ValueChanged onColorChanged; + + /// The orientation of the color picker layout + final Axis orientation; + + /// The shape of the color spectrum + final ColorSpectrumShape colorSpectrumShape; + + /// Whether the color preview is visible + final bool isColorPreviewVisible; + + /// The minimum allowed hue value (0-359) + final int minHue; + + /// The maximum allowed hue value (0-359) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// Creates a new instance of [_ColorSpectrumAndPreview]. + const _ColorSpectrumAndPreview({ + required this.colorState, + required this.colorSpectrumShape, + required this.onColorChanged, + required this.orientation, + required this.isColorPreviewVisible, + required this.minHue, + required this.maxHue, + required this.minSaturation, + required this.maxSaturation, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: (orientation == Axis.horizontal) && !isColorPreviewVisible + ? ColorPickerSizes.spectrum + : ColorPickerSizes.maxWidth, + height: ColorPickerSizes.spectrum, + child: isColorPreviewVisible + ? Row( + children: [ + _buildSpectrum(), + const SizedBox(width: ColorPickerSpacing.small), + _buildPreviewBox(context), + ], + ) + : Center(child: _buildSpectrum()), + ); + } + + Widget _buildSpectrum() { + return SizedBox( + width: ColorPickerSizes.spectrum, + height: ColorPickerSizes.spectrum, + child: colorSpectrumShape == ColorSpectrumShape.ring + ? ColorRingSpectrum( + colorState: colorState, + onColorChanged: onColorChanged, + minHue: minHue, + maxHue: maxHue, + minSaturation: minSaturation, + maxSaturation: maxSaturation, + ) + : ColorBoxSpectrum( + colorState: colorState, + onColorChanged: onColorChanged, + minHue: minHue, + maxHue: maxHue, + minSaturation: minSaturation, + maxSaturation: maxSaturation, + ), + ); + } + + Widget _buildPreviewBox(BuildContext context) { + const double width = ColorPickerSizes.preview; + const double height = ColorPickerSizes.spectrum; + const double borderRadius = 4.0; + + final theme = FluentTheme.of(context); + final color = colorState.toColor(); + + return Container( + width: width, + height: height, + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.dividerStrokeColorDefault, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(borderRadius - 1), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint( + painter: CheckerboardPainter(), + ), + ), + Positioned.fill( + child: Container( + color: color, + ), + ), + ], + ), + ), + ); + } +} + +/// A widget that displays the color sliders. +class _ColorSliders extends StatelessWidget { + /// The current color state + final ColorState colorState; + + /// Callback when the color changes + final ValueChanged onColorChanged; + + /// The orientation of the color picker layout + final Axis orientation; + + /// Whether the color slider is visible + final bool isColorSliderVisible; + + /// Whether the alpha slider is visible + final bool isAlphaSliderVisible; + + /// Whether the alpha channel is enabled + final bool isAlphaEnabled; + + /// The minimum allowed HSV value (0-100) + final int minValue; + + /// The maximum allowed HSV value (0-100) + final int maxValue; + + /// Creates a new instance of [_ColorSliders]. + const _ColorSliders({ + required this.colorState, + required this.onColorChanged, + required this.orientation, + required this.isColorSliderVisible, + required this.isAlphaSliderVisible, + required this.isAlphaEnabled, + required this.minValue, + required this.maxValue, + }); + + @override + Widget build(BuildContext context) { + final thumbColor = FluentTheme.of(context).resources.focusStrokeColorOuter; + // Determine if the sliders should be displayed horizontally or vertically + final bool isVertical = orientation != Axis.vertical; + + final sliders = [ + if (isColorSliderVisible) _buildValueSlider(thumbColor, isVertical), + if (isColorSliderVisible && isAlphaSliderVisible && isAlphaEnabled) + orientation == Axis.vertical + ? const SizedBox(height: ColorPickerSpacing.large) + : const SizedBox( + width: ColorPickerSpacing.large, + ), + if (isAlphaSliderVisible && isAlphaEnabled) + _buildAlphaSlider(thumbColor, isVertical), + ]; + + return orientation == Axis.horizontal + ? Row( + mainAxisSize: MainAxisSize.min, + children: sliders, + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: sliders, + ); + } + + /// Builds the value slider for the color picker. + Widget _buildValueSlider(Color thumbColor, bool isVertical) { + final colorName = colorState.guessColorName(); + final valueText = + '${(colorState.value * 100).round()}% ${colorName.isNotEmpty ? "($colorName)" : ""}'; + + return SizedBox( + width: isVertical ? ColorPickerSizes.slider : ColorPickerSizes.maxWidth, + height: isVertical ? ColorPickerSizes.spectrum : ColorPickerSizes.slider, + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: isVertical + ? Alignment.bottomCenter + : Alignment.centerLeft, + end: isVertical ? Alignment.topCenter : Alignment.centerRight, + colors: [ + const Color(0xFF000000), + HSVColor.fromAHSV(1, math.max(0, colorState.hue), + math.max(0, colorState.saturation), 1.0) + .toColor(), + ], + ), + ), + ), + SliderTheme( + data: SliderThemeData( + activeColor: WidgetStatePropertyAll(thumbColor), + trackHeight: const WidgetStatePropertyAll(0.0), + ), + child: Slider( + label: valueText, + vertical: isVertical, + value: colorState.value, + min: minValue / 100, + max: maxValue / 100, + onChanged: (value) => + onColorChanged(colorState.copyWith(value: value)), + ), + ), + ], + ), + ), + ); + } + + /// Builds the alpha slider for the color picker. + Widget _buildAlphaSlider(Color thumbColor, bool isVertical) { + final opacityText = '${(colorState.alpha * 100).round()}% opacity'; + + return SizedBox( + width: isVertical + ? ColorPickerSizes.slider + : ColorPickerSizes.spectrum + + ColorPickerSpacing.small + + ColorPickerSizes.preview, + height: isVertical ? ColorPickerSizes.spectrum : ColorPickerSizes.slider, + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Stack( + children: [ + Positioned.fill( + child: CustomPaint(painter: CheckerboardPainter()), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: isVertical + ? Alignment.bottomCenter + : Alignment.centerLeft, + end: isVertical ? Alignment.topCenter : Alignment.centerRight, + colors: [ + colorState.toColor().withAlpha(0), + colorState.toColor().withAlpha(255), + ], + ), + ), + ), + SliderTheme( + data: SliderThemeData( + activeColor: WidgetStatePropertyAll(thumbColor), + trackHeight: const WidgetStatePropertyAll(0.0), + ), + child: Slider( + label: opacityText, + vertical: isVertical, + value: colorState.alpha, + min: 0, + max: 1, + onChanged: (value) => + onColorChanged(colorState.copyWith(alpha: value)), + ), + ), + ], + ), + ), + ); + } +} + +/// A widget that displays the color inputs. +class _ColorInputs extends StatelessWidget { + /// Map of color modes (RGB and HSV) + static const Map colorModes = { + 'RGB': ColorMode.rgb, + 'HSV': ColorMode.hsv, + }; + + /// Internal ValueNotifier for color mode management + static final _colorModeNotifier = ValueNotifier(ColorMode.rgb); + + /// The current color state + final ColorState colorState; + + /// Callback when the color changes + final ValueChanged onColorChanged; + + /// The orientation of the color picker layout + final Axis orientation; + + /// Whether the "More" button is expanded + final bool isMoreExpanded; + + /// Whether the "More" button is visible + final bool isMoreButtonVisible; + + /// Whether the hex input is visible + final bool isHexInputVisible; + + /// Whether the color channel text input is visible + final bool isColorChannelTextInputVisible; + + /// Whether the alpha channel is enabled + final bool isAlphaEnabled; + + /// Whether the alpha text input is visible + final bool isAlphaTextInputVisible; + + /// Controller for the hex input + final TextEditingController hexController; + + /// The minimum allowed hue value (0-359) + final int minHue; + + /// The maximum allowed hue value (0-359) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// The minimum allowed value/brightness (0-100) + final int minValue; + + /// The maximum allowed value/brightness (0-100) + final int maxValue; + + /// Creates a new instance of [ColorInputs]. + const _ColorInputs({ + required this.colorState, + required this.onColorChanged, + required this.orientation, + required this.isMoreExpanded, + required this.isMoreButtonVisible, + required this.isHexInputVisible, + required this.isColorChannelTextInputVisible, + required this.isAlphaEnabled, + required this.isAlphaTextInputVisible, + required this.minHue, + required this.maxHue, + required this.minSaturation, + required this.maxSaturation, + required this.minValue, + required this.maxValue, + required this.hexController, + }); + + @override + Widget build(BuildContext context) { + // Update hex input whenever colorState changes + _updateHexControllerText(); + + return ValueListenableBuilder( + valueListenable: _colorModeNotifier, + builder: (context, colorMode, _) { + final inputsContent = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (orientation != Axis.vertical || + !isMoreButtonVisible || + isMoreExpanded) ...[ + _buildColorModeAndHexInput(colorMode), + colorMode == ColorMode.rgb + ? _buildRGBInputs() + : _buildHSVInputs(), + ], + ], + ); + + return orientation == Axis.vertical + ? SizedBox( + width: ColorPickerSizes.maxWidth, + child: inputsContent, + ) + : SizedBox( + height: ColorPickerSizes.spectrum, + width: 200, // arbitrary width, but more than enough + child: inputsContent, + ); + }, + ); + } + + /// Updates the hex controller text based on current color state + void _updateHexControllerText() { + final currentHex = colorState.toHexString(isAlphaEnabled); + if (hexController.text != currentHex) { + // Use text setter instead of direct assignment to handle text selection + final selection = hexController.selection; + hexController.text = currentHex; + // Maintain cursor position if it was in a valid range + if (selection.isValid && selection.start <= currentHex.length) { + hexController.selection = selection; + } + } + } + + /// Builds the color mode selector and hex input. + Widget _buildColorModeAndHexInput(ColorMode colorMode) { + final modeSelector = SizedBox( + width: ColorPickerSizes.inputBox, + child: ComboBox( + value: colorMode, + items: colorModes.entries + .map((e) => ComboBoxItem(value: e.value, child: Text(e.key))) + .toList(), + onChanged: (value) { + if (value != null) _colorModeNotifier.value = value; + }, + ), + ); + + final hexInput = SizedBox( + width: ColorPickerSizes.inputBox, + child: TextBox( + controller: hexController, + placeholder: isAlphaEnabled ? '#AARRGGBB' : '#RRGGBB', + onSubmitted: _updateHexColor, + )); + + return orientation == Axis.vertical + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (isColorChannelTextInputVisible) ...[ + modeSelector, + ], + if (isHexInputVisible) ...[ + hexInput, + ], + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isHexInputVisible) ...[ + Align( + alignment: Alignment.centerLeft, + child: hexInput, + ), + const SizedBox(height: ColorPickerSpacing.small), + ], + if (isColorChannelTextInputVisible) ...[ + Align( + alignment: Alignment.centerLeft, + child: modeSelector, + ), + ], + ], + ); + } + + /// Builds the RGB input fields. + Widget _buildRGBInputs() { + // TODO: localize color channel labels + return Column( + children: [ + if (isColorChannelTextInputVisible) ...{ + _buildNumberInput( + 'Red', + colorState.red * 255, + (v) { + final newState = colorState.copyWith(red: v / 255); + onColorChanged(newState); + }, + min: 0, + max: 255, + ), + _buildNumberInput( + 'Green', + colorState.green * 255, + (v) { + final newState = colorState.copyWith(green: v / 255); + onColorChanged(newState); + }, + min: 0, + max: 255, + ), + _buildNumberInput( + 'Blue', + colorState.blue * 255, + (v) { + final newState = colorState.copyWith(blue: v / 255); + onColorChanged(newState); + }, + min: 0, + max: 255, + ), + }, + if (isAlphaEnabled && isAlphaTextInputVisible) ...[ + _buildNumberInput( + 'Opacity', + colorState.alpha * 100, + (v) { + final newState = colorState.copyWith(alpha: v / 100); + onColorChanged(newState); + }, + min: 0, + max: 100, + ), + ], + ], + ); + } + + /// Builds the HSV input fields. + Widget _buildHSVInputs() { + return Column( + children: [ + if (isColorChannelTextInputVisible) ...{ + _buildNumberInput( + 'Hue', + colorState.hue, + (v) { + final newState = colorState.copyWith(hue: v); + onColorChanged(newState); + }, + min: minHue.toDouble(), + max: maxHue.toDouble(), + ), + _buildNumberInput( + 'Saturation', + colorState.saturation * 100, + (v) { + final newState = colorState.copyWith(saturation: v / 100); + onColorChanged(newState); + }, + min: minSaturation.toDouble(), + max: maxSaturation.toDouble(), + ), + _buildNumberInput( + 'Value', + colorState.value * 100, + (v) { + final newState = colorState.copyWith(value: v / 100); + onColorChanged(newState); + }, + min: minValue.toDouble(), + max: maxValue.toDouble(), + ), + }, + if (isAlphaEnabled && isAlphaTextInputVisible) ...[ + _buildNumberInput( + 'Opacity', + colorState.alpha * 100, + (v) { + final newState = colorState.copyWith(alpha: v / 100); + onColorChanged(newState); + }, + min: 0, + max: 100, + ), + ], + ], + ); + } + + /// Builds a number input field. + Widget _buildNumberInput( + String label, + double value, + Function(double) onChanged, { + required double min, + required double max, + }) { + // TODO: initial format issue of NumberBox not being applied. + return Column(children: [ + const SizedBox(height: ColorPickerSpacing.small), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: ColorPickerSizes.inputBox, + child: NumberBox( + value: value, + min: min, + max: max, + mode: SpinButtonPlacementMode.none, + clearButton: false, + onChanged: (v) { + if (v == null || v.isNaN || v.isInfinite) return; + onChanged(v); + }, + format: (v) => v?.round().toString(), + ), + ), + const SizedBox(width: 5), + Text(label), + ], + ), + ]); + } + + /// Updates the hex color value. + void _updateHexColor(String text) { + // Skip if the text is not a valid hex color length + final expectedLength = isAlphaEnabled ? 9 : 7; + if (text.length != expectedLength) { + // Revert to current valid hex + _updateHexControllerText(); + return; + } + + try { + final cleanText = text.startsWith('#') ? text.substring(1) : text; + + // Parse hex string with or without alpha + int colorValue; + double a = colorState.alpha; // Preserve existing alpha + + if (cleanText.length == 6) { + // RGB format: Parse RGB and keep existing alpha + colorValue = int.parse(cleanText, radix: 16); + } else { + // ARGB format: Parse both alpha and RGB + colorValue = int.parse(cleanText, radix: 16); + a = ((colorValue >> 24) & 0xFF) / 255.0; + } + + // Extract color components + final r = ((colorValue >> 16) & 0xFF) / 255.0; + final g = ((colorValue >> 8) & 0xFF) / 255.0; + final b = (colorValue & 0xFF) / 255.0; + + // Convert RGB to HSV + final (h, s, v) = ColorState.rgbToHsv(r, g, b); + + // Create new ColorState + final newState = ColorState(r, g, b, a, h, s, v); + onColorChanged(newState); + } catch (e) { + debugPrint('Error parsing hex color: $e'); + // Revert to the current color's hex value + final currentHex = colorState.toHexString(isAlphaEnabled); + if (hexController.text != currentHex) { + hexController.text = currentHex; + } + } + } +} diff --git a/lib/src/controls/form/color_picker/color_spectrum.dart b/lib/src/controls/form/color_picker/color_spectrum.dart new file mode 100644 index 000000000..603366bcb --- /dev/null +++ b/lib/src/controls/form/color_picker/color_spectrum.dart @@ -0,0 +1,760 @@ +import 'dart:math' as math; +import 'dart:ui' as dart; + +import 'package:fluent_ui/fluent_ui.dart'; + +import 'color_state.dart'; + +/// A widget that displays a color ring spectrum for selecting hue and saturation. +/// +/// This widget allows users to select a color by interacting with a color ring. +/// The hue is selected by rotating around the ring, and the saturation is selected +/// by moving towards or away from the center of the ring. +class ColorRingSpectrum extends StatefulWidget { + /// The current color state + final ColorState colorState; + + /// Callback when the color changes + final ValueChanged onColorChanged; + + /// The minimum allowed hue value (0-360) + final int minHue; + + /// The maximum allowed hue value (0-360) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// Creates a new instance of [ColorRingSpectrum]. + /// + /// - [colorState]: The current color state. + /// - [onColorChanged]: Callback when the color changes. + /// - [minHue]: The minimum allowed hue value (0-360). + /// - [maxHue]: The maximum allowed hue value (0-360). + /// - [minSaturation]: The minimum allowed saturation value (0-100). + /// - [maxSaturation]: The maximum allowed saturation value (0-100). + const ColorRingSpectrum({ + super.key, + required this.colorState, + required this.onColorChanged, + this.minHue = 0, + this.maxHue = 360, + this.minSaturation = 0, + this.maxSaturation = 100, + }) : assert(minHue >= 0 && minHue <= maxHue && maxHue <= 360, + 'Hue values must be between 0 and 360'), + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'); + + @override + State createState() => _ColorRingSpectrumState(); +} + +class _ColorRingSpectrumState extends State { + bool _showLabel = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onPanStart: _handlePanStart, + onPanUpdate: _handlePanUpdate, + onPanEnd: _handlePanEnd, + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + child: CustomPaint( + painter: _RingSpectrumPainter( + colorState: widget.colorState, + showLabel: _showLabel, + theme: FluentTheme.of(context), + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + ), + ), + ); + } + + void _handlePanStart(DragStartDetails details) { + setState(() => _showLabel = true); + _updateColorFromWheel(details.localPosition); + } + + void _handlePanUpdate(DragUpdateDetails details) { + _updateColorFromWheel(details.localPosition); + } + + void _handlePanEnd(DragEndDetails details) { + setState(() => _showLabel = false); + } + + void _handleTapDown(TapDownDetails details) { + setState(() => _showLabel = true); + _updateColorFromWheel(details.localPosition); + } + + void _handleTapUp(TapUpDetails details) { + setState(() => _showLabel = false); + } + + void _updateColorFromWheel(Offset position) { + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final Size size = renderBox.size; + + final Offset center = Offset(size.width / 2, size.height / 2); + final double radius = math.min(size.width, size.height) / 2; + + // Calculate distance from center + double x = position.dx - center.dx; + double y = position.dy - center.dy; + double distance = math.sqrt(x * x + y * y); + + // If the point is outside the wheel, bring it back into the circle + if (distance > radius) { + x *= (radius / distance); + y *= (radius / distance); + distance = radius; + } + + // Calculate angle and map it directly to minHue~maxHue range + double angle = math.atan2(y, x) * 180 / math.pi; + angle = (angle + 360) % 360; + + // Map the 0-360 angle range to minHue-maxHue range + final h = widget.minHue + (angle / 360) * (widget.maxHue - widget.minHue); + + // Map the 0-1 distance range to minSaturation-maxSaturation range + final normalizedDistance = distance / radius; + final s = normalizedDistance * + (widget.maxSaturation - widget.minSaturation) / + 100 + + widget.minSaturation / 100; + + // Note: HSL value is not set in the box spectrum. + widget.colorState.setHue(h); + widget.colorState.setSaturation(s); + + widget.onColorChanged(widget.colorState); + } +} + +/// A widget that displays a color box spectrum for selecting hue and saturation. +/// +/// This widget allows users to select a color by interacting with a color box. +/// The hue is selected by moving horizontally, and the saturation is selected +/// by moving vertically. +class ColorBoxSpectrum extends StatefulWidget { + /// The current color state + final ColorState colorState; + + /// Callback when the color changes + final ValueChanged onColorChanged; + + /// The minimum allowed hue value (0-360) + final int minHue; + + /// The maximum allowed hue value (0-360) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// Creates a new instance of [ColorBoxSpectrum]. + /// + /// - [colorState]: The current color state. + /// - [onColorChanged]: Callback when the color changes. + /// - [minHue]: The minimum allowed hue value (0-360). + /// - [maxHue]: The maximum allowed hue value (0-360). + /// - [minSaturation]: The minimum allowed saturation value (0-100). + /// - [maxSaturation]: The maximum allowed saturation value (0-100). + const ColorBoxSpectrum({ + super.key, + required this.colorState, + required this.onColorChanged, + this.minHue = 0, + this.maxHue = 360, + this.minSaturation = 0, + this.maxSaturation = 100, + }) : assert(minHue >= 0 && minHue <= maxHue && maxHue <= 360, + 'Hue values must be between 0 and 360'), + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'); + + @override + State createState() => _ColorBoxSpectrumState(); +} + +class _ColorBoxSpectrumState extends State { + bool _showLabel = false; + + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + + return GestureDetector( + onPanStart: _handlePanStart, + onPanUpdate: _handlePanUpdate, + onPanEnd: _handlePanEnd, + onTapDown: _handleTapDown, + onTapUp: _handleTapUp, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.dividerStrokeColorDefault, + ), + borderRadius: BorderRadius.circular(4), + ), + child: CustomPaint( + painter: _BoxSpectrumPainter( + colorState: widget.colorState, + showLabel: _showLabel, + theme: theme, + minHue: widget.minHue, + maxHue: widget.maxHue, + minSaturation: widget.minSaturation, + maxSaturation: widget.maxSaturation, + ), + ), + ), + ); + } + + void _handlePanStart(DragStartDetails details) { + setState(() => _showLabel = true); + _updateColorFromBox(details.localPosition); + } + + void _handlePanUpdate(DragUpdateDetails details) { + _updateColorFromBox(details.localPosition); + } + + void _handlePanEnd(DragEndDetails details) { + setState(() => _showLabel = false); + } + + void _handleTapDown(TapDownDetails details) { + setState(() => _showLabel = true); + _updateColorFromBox(details.localPosition); + } + + void _handleTapUp(TapUpDetails details) { + setState(() => _showLabel = false); + } + + void _updateColorFromBox(Offset position) { + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final Size size = renderBox.size; + + final double width = size.width; + final double height = size.height; + + // Clamp position within bounds + final double x = position.dx.clamp(0, width); + final double y = position.dy.clamp(0, height); + + // Calculate HSV values + // Hue from left to right (minHue to maxHue) + final double h = + widget.minHue + (x / width) * (widget.maxHue - widget.minHue); + // Saturation from top (maxSaturation) to bottom (minSaturation) + final double s = widget.maxSaturation / 100 - + (y / height) * (widget.maxSaturation - widget.minSaturation) / 100; + + // Note: HSL value is not set in the box spectrum. + widget.colorState.setHue(h); + widget.colorState.setSaturation(s); + + widget.onColorChanged(widget.colorState); + } +} + +/// Custom painter for rendering a ring-shaped color spectrum. +/// +/// This painter draws a ring-shaped gradient representing a spectrum of hues +/// and saturations. It also supports displaying an indicator for the currently +/// selected color and an optional label with the color name during interaction. +class _RingSpectrumPainter extends CustomPainter { + /// The current color state + final ColorState colorState; + + /// Whether to show the color name label + final bool showLabel; + + /// The theme data for styling + final FluentThemeData theme; + + /// The minimum allowed hue value (0-360) + final int minHue; + + /// The maximum allowed hue value (0-360) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// Creates a new instance of [_RingSpectrumPainter]. + _RingSpectrumPainter({ + required this.colorState, + required this.showLabel, + required this.theme, + this.minHue = 0, + this.maxHue = 360, + this.minSaturation = 0, + this.maxSaturation = 1, + }) : assert(minHue >= 0 && minHue <= maxHue && maxHue <= 360, + 'Hue values must be between 0 and 360'), + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'), + super(repaint: colorState); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = math.min(size.width, size.height) / 2; + + // Draw color wheel with saturation + for (double angle = 0; angle < 360; angle += 0.2) { + final radians = angle * math.pi / 180; + + final mappedHue = minHue + (angle / 360) * (maxHue - minHue); + final shader = RadialGradient( + center: Alignment.center, + radius: 1.0, + stops: const [0.0, 0.5], + colors: [ + HSVColor.fromAHSV(1.0, mappedHue, minSaturation / 100, 1.0).toColor(), + HSVColor.fromAHSV(1.0, mappedHue, maxSaturation / 100, 1.0).toColor(), + ], + ).createShader(Rect.fromCircle(center: center, radius: radius)); + + final paint = Paint() + ..shader = shader + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + radians, + 0.02, + true, + paint, + ); + } + + // Draw current color indicator + final normalizedHue = (maxHue == minHue) + ? maxHue + : (colorState.hue - minHue) / (maxHue - minHue) * 360; // [0..360] + final normalizedSaturation = (maxSaturation == minSaturation) + ? maxSaturation / 100 + : (colorState.saturation * 100 - minSaturation) / + (maxSaturation - minSaturation); // [0..1] + + final radians = normalizedHue * math.pi / 180.0; + final distance = normalizedSaturation * radius; + final indicatorOffset = Offset( + center.dx + math.cos(radians) * distance, + center.dy + math.sin(radians) * distance, + ); + + // Draw indicator with current color and border + // Calculate perceived brightness to determine stroke color + final rgb = ColorState.hsvToRgb(colorState.hue, colorState.saturation, 1.0); + final fillColor = Color.fromARGB(255, (rgb.$1 * 255).round(), + (rgb.$2 * 255).round(), (rgb.$3 * 255).round()); + final double brightness = 0.299 * rgb.$1 + 0.587 * rgb.$2 + 0.114 * rgb.$3; + final strokeColor = brightness > 0.5 ? Colors.black : Colors.white; + + // Draw white circle with black border for indicator + canvas.drawCircle( + indicatorOffset, + 8, + Paint() + ..color = strokeColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + canvas.drawCircle( + indicatorOffset, + 7, + Paint()..color = fillColor, + ); + + // Draw color name label if needed + if (showLabel) { + final colorName = colorState.guessColorName(); + _drawLabel(canvas, size, colorName, indicatorOffset); + } + } + + @override + bool shouldRepaint(covariant _RingSpectrumPainter oldDelegate) { + return colorState != oldDelegate.colorState || + showLabel != oldDelegate.showLabel || + theme != oldDelegate.theme; + } + + void _drawLabel(Canvas canvas, Size size, String text, Offset position) { + final backgroundColor = theme.resources.controlSolidFillColorDefault; + final textColor = theme.resources.textFillColorPrimary; + final borderRadius = BorderRadius.circular(4.0); + const labelPadding = EdgeInsets.symmetric(horizontal: 8, vertical: 4); + + final textSpan = TextSpan( + text: text, + style: TextStyle(color: textColor), + ); + + final textPainter = TextPainter( + text: textSpan, + textAlign: TextAlign.left, + textDirection: dart.TextDirection + .ltr, // TODO: Update if color names support RTL languages in the future. + )..layout(); + + final labelWidth = textPainter.width + labelPadding.horizontal; + final labelHeight = textPainter.height + labelPadding.vertical; + final labelX = (position.dx - labelWidth / 2) + .clamp(0, size.width - labelWidth) + .toDouble(); + double labelY = (position.dy - labelHeight - 30) + .clamp(0, size.height - labelHeight) + .toDouble(); + + // Check if label would overlap the indicator and adjust position + final labelBottomY = labelY + labelHeight; + if (position.dy < labelBottomY) { + labelY = position.dy + labelHeight - 5; + } + + final rect = Rect.fromLTWH(labelX, labelY, labelWidth, labelHeight); + + // Draw background with shadow + final shadow = BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ); + + // Draw shadow + canvas.drawRRect( + RRect.fromRectAndCorners( + rect.shift(shadow.offset).inflate(shadow.spreadRadius), + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ), + Paint() + ..color = shadow.color + ..maskFilter = MaskFilter.blur( + BlurStyle.normal, + shadow.blurRadius, + ), + ); + + // Draw background + canvas.drawRRect( + RRect.fromRectAndCorners( + rect, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ), + Paint()..color = backgroundColor, + ); + + // Draw text + textPainter.paint( + canvas, + Offset( + labelX + labelPadding.left, + labelY + labelPadding.top, + ), + ); + } +} + +/// Custom painter for rendering a box-shaped color spectrum. +/// +/// This painter draws a rectangular color spectrum with gradients representing +/// hue (left to right) and saturation (top to bottom). It also supports displaying +/// an indicator for the currently selected color and an optional label with the +/// color name during interaction. +class _BoxSpectrumPainter extends CustomPainter { + /// The current color state + final ColorState colorState; + + /// Whether to show the color name label + final bool showLabel; + + /// The theme data for styling + final FluentThemeData theme; + + /// The minimum allowed hue value (0-360) + final int minHue; + + /// The maximum allowed hue value (0-360) + final int maxHue; + + /// The minimum allowed saturation value (0-100) + final int minSaturation; + + /// The maximum allowed saturation value (0-100) + final int maxSaturation; + + /// Creates a new instance of [_BoxSpectrumPainter]. + _BoxSpectrumPainter({ + required this.colorState, + required this.showLabel, + required this.theme, + this.minHue = 0, + this.maxHue = 360, + this.minSaturation = 0, + this.maxSaturation = 100, + }) : assert(minHue >= 0 && minHue <= maxHue && maxHue <= 360, + 'Hue values must be between 0 and 360'), + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'), + super(repaint: colorState); + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + + // Draw hue gradient (left to right) + final colors = List.generate(360, (index) { + final hue = minHue + (index / 360) * (maxHue - minHue); + return HSVColor.fromAHSV(1.0, hue, 1.0, 1.0).toColor(); + }); + final hueGradient = LinearGradient( + colors: colors, + ); + + canvas.drawRect( + rect, + Paint()..shader = hueGradient.createShader(rect), + ); + + // Draw brightness gradient (top to bottom) + final saturationGradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: [(1 - maxSaturation / 100), (1 - minSaturation / 100)], + colors: const [ + Colors.transparent, + Colors.white, + ], + ); + + canvas.drawRect( + rect, + Paint()..shader = hueGradient.createShader(rect), + ); + + canvas.drawRect( + rect, + Paint()..shader = saturationGradient.createShader(rect), + ); + + // Draw current color indicator + // Map the current hue and saturation to the box coordinates + final normalizedHue = (colorState.hue - minHue) / (maxHue - minHue); + final normalizedSaturation = (colorState.saturation * 100 - minSaturation) / + (maxSaturation - minSaturation); + + final x = normalizedHue * size.width; + final y = (1 - normalizedSaturation) * size.height; + + // Draw indicator with current color and white border + // Calculate perceived brightness to determine stroke color + final rgb = ColorState.hsvToRgb(colorState.hue, colorState.saturation, 1.0); + final fillColor = Color.fromARGB(255, (rgb.$1 * 255).round(), + (rgb.$2 * 255).round(), (rgb.$3 * 255).round()); + final double brightness = 0.299 * rgb.$1 + 0.587 * rgb.$2 + 0.114 * rgb.$3; + final strokeColor = brightness > 0.5 ? Colors.black : Colors.white; + + // Draw white circle with black border for indicator + canvas.drawCircle( + Offset(x, y), + 8, + Paint() + ..color = strokeColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + canvas.drawCircle( + Offset(x, y), + 7, + Paint()..color = fillColor, + ); + + // Draw color name label if needed + if (showLabel) { + final colorName = colorState.guessColorName(); + _drawLabel(canvas, size, colorName, Offset(x, y)); + } + } + + @override + bool shouldRepaint(covariant _BoxSpectrumPainter oldDelegate) { + return colorState != oldDelegate.colorState || + showLabel != oldDelegate.showLabel || + theme != oldDelegate.theme; + } + + void _drawLabel(Canvas canvas, Size size, String text, Offset position) { + final backgroundColor = theme.resources.controlSolidFillColorDefault; + final textColor = theme.resources.textFillColorPrimary; + final borderRadius = BorderRadius.circular(4.0); + const labelPadding = EdgeInsets.symmetric(horizontal: 8, vertical: 4); + + final textSpan = TextSpan( + text: text, + style: TextStyle(color: textColor), + ); + + final textPainter = TextPainter( + text: textSpan, + textAlign: TextAlign.left, + textDirection: dart.TextDirection + .ltr, // TODO: Update if color names support RTL languages in the future. + )..layout(); + + final labelWidth = textPainter.width + labelPadding.horizontal; + final labelHeight = textPainter.height + labelPadding.vertical; + final labelX = (position.dx - labelWidth / 2) + .clamp(0, size.width - labelWidth) + .toDouble(); + double labelY = (position.dy - labelHeight - 30) + .clamp(0, size.height - labelHeight) + .toDouble(); + + // Check if label would overlap the indicator and adjust position + final labelBottomY = labelY + labelHeight; + if (position.dy < labelBottomY) { + labelY = position.dy + labelHeight - 5; + } + + final rect = Rect.fromLTWH(labelX, labelY, labelWidth, labelHeight); + + // Draw background with shadow + final shadow = BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ); + + // Draw shadow + canvas.drawRRect( + RRect.fromRectAndCorners( + rect.shift(shadow.offset).inflate(shadow.spreadRadius), + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ), + Paint() + ..color = shadow.color + ..maskFilter = MaskFilter.blur( + BlurStyle.normal, + shadow.blurRadius, + ), + ); + + // Draw background + canvas.drawRRect( + RRect.fromRectAndCorners( + rect, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ), + Paint()..color = backgroundColor, + ); + + // Draw text + textPainter.paint( + canvas, + Offset( + labelX + labelPadding.left, + labelY + labelPadding.top, + ), + ); + } +} + +/// Custom painter for drawing a checkerboard pattern. +/// +/// This painter is used to represent transparency in the [ColorPicker]'s alpha slider. +class CheckerboardPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + const squareSize = 5; + final paint = Paint(); + final width = size.width.toInt(); + final height = size.height.toInt(); + + for (int i = 0; i < width; i += (squareSize * 2)) { + for (int j = 0; j < height; j += (squareSize * 2)) { + // Draw dark squares + paint.color = const Color(0xFFD3D3D3); // Light gray + canvas.drawRect( + Rect.fromLTWH(i.toDouble(), j.toDouble(), squareSize.toDouble(), + squareSize.toDouble()), + paint, + ); + canvas.drawRect( + Rect.fromLTWH( + (i + squareSize).toDouble(), + (j + squareSize).toDouble(), + squareSize.toDouble(), + squareSize.toDouble()), + paint, + ); + + // Draw light squares + paint.color = Colors.white; + canvas.drawRect( + Rect.fromLTWH((i + squareSize).toDouble(), j.toDouble(), + squareSize.toDouble(), squareSize.toDouble()), + paint, + ); + canvas.drawRect( + Rect.fromLTWH(i.toDouble(), (j + squareSize).toDouble(), + squareSize.toDouble(), squareSize.toDouble()), + paint, + ); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/src/controls/form/color_picker/color_state.dart b/lib/src/controls/form/color_picker/color_state.dart new file mode 100644 index 000000000..bfd92172d --- /dev/null +++ b/lib/src/controls/form/color_picker/color_state.dart @@ -0,0 +1,520 @@ +import 'dart:math' as math; + +import 'package:fluent_ui/fluent_ui.dart'; + +part "color_names.dart"; + +/// A stateful representation of a color in both RGB and HSV color spaces. +class ColorState extends ChangeNotifier { + // RGB components (0-1) + late double _red; // 0–1 + late double _green; // 0–1 + late double _blue; // 0–1 + late double _alpha; // 0–1 + + // HSV components + late double _hue; // 0–360 + late double _saturation; // 0–1 + late double _value; // 0–1 + + /// Gets the red component (0–1). + double get red => _red; + + /// Gets the green component (0–1). + double get green => _green; + + /// Gets the blue component (0–1). + double get blue => _blue; + + /// Gets the alpha (transparency) component (0–1). + double get alpha => _alpha; + + /// Gets the hue (0–360). + double get hue => _hue; + + /// Gets the saturation (0–1). + double get saturation => _saturation; + + /// Gets the value/brightness (0–1). + double get value => _value; + + /// Constructs a [ColorState] with the given components. + /// + /// All components must be within their valid ranges: + /// - [red], [green], [blue], [alpha], [saturation], [value]: 0–1 + /// - [hue]: 0–360 + ColorState(this._red, this._green, this._blue, this._alpha, this._hue, + this._saturation, this._value) { + _validateColorValues(); + } + + /// Creates a [ColorState] from a [Color]. + static ColorState fromColor(Color color) { + final r = color.red.toDouble() / 255; + final g = color.green.toDouble() / 255; + final b = color.blue.toDouble() / 255; + final a = color.alpha.toDouble() / 255; + + final (h, s, v) = rgbToHsv(r, g, b); + return ColorState(r, g, b, a, h, s, v); + } + + /// Sets the hue and updates the RGB values accordingly. + void setHue(double newValue) { + _hue = (newValue + 360) % 360; + _recalculateRGBFromHSV(); + notifyListeners(); + } + + /// Sets the saturation and updates the RGB values accordingly. + void setSaturation(double newValue) { + _saturation = newValue.clamp(0, 1); + _recalculateRGBFromHSV(); + notifyListeners(); + } + + /// Sets the value and updates the RGB values accordingly. + void setValue(double newValue) { + _value = newValue.clamp(0, 1); + + if (_value == 0) { + _red = 0; + _green = 0; + _blue = 0; + } else { + final (r, g, b) = hsvToRgb(_hue, _saturation, _value); + _red = r; + _green = g; + _blue = b; + } + + notifyListeners(); + } + + /// Sets the red component and updates the HSV values accordingly. + void setRed(double newValue) { + _red = newValue.clamp(0, 1); + _recalculateHSVFromRGB(); + notifyListeners(); + } + + /// Sets the green component and updates the HSV values accordingly. + void setGreen(double newValue) { + _green = newValue.clamp(0, 1); + _recalculateHSVFromRGB(); + notifyListeners(); + } + + /// Sets the blue component and updates the HSV values accordingly. + void setBlue(double newValue) { + _blue = newValue.clamp(0, 1); + _recalculateHSVFromRGB(); + notifyListeners(); + } + + /// Sets the alpha component (0–1). + void setAlpha(double newValue) { + _alpha = newValue.clamp(0, 1); + notifyListeners(); + } + + /// Sets the color from a hexadecimal color string. + void setHex(String hexText) { + try { + final text = hexText.startsWith('#') ? hexText.substring(1) : hexText; + + // check if the text has an alpha channel + final colorText = text.length == 6 ? 'FF$text' : text; + final color = Color(int.parse(colorText, radix: 16)); + + _red = color.red / 255; + _green = color.green / 255; + _blue = color.blue / 255; + _alpha = color.alpha / 255; + + final (h, s, v) = rgbToHsv(_red, _green, _blue); + _hue = h; + _saturation = s; + _value = v; + + notifyListeners(); + } catch (e) { + return; + } + } + + /// Converts the current state to a [Color] object. + Color toColor() { + final a = (_alpha * 255).round(); + + if (_value == 0) { + return Color.fromARGB(a, 0, 0, 0); + } + + final r = (_red * 255).round(); + final g = (_green * 255).round(); + final b = (_blue * 255).round(); + return Color.fromARGB(a, r, g, b); + } + + /// Guess the name of the color based on the current RGB values. + String guessColorName() { + try { + final rgb1 = (red: _red, green: _green, blue: _blue); + final hsl1 = rgbToHsl(_red, _green, _blue); + + double minDistance = double.infinity; + String closestColorName = ''; + + for (final entry in _ColorNames._values.entries) { + final hexColor = entry.key; + final name = entry.value; + + final r = ((hexColor >> 16) & 0xFF) / 255.0; + final g = ((hexColor >> 8) & 0xFF) / 255.0; + final b = (hexColor & 0xFF) / 255.0; + + final rgb2 = (red: r, green: g, blue: b); + final hsl2 = rgbToHsl(r, g, b); + + final distance = _rgbHslDistance(rgb1, hsl1, rgb2, hsl2); + if (distance < minDistance) { + minDistance = distance; + closestColorName = name; + } + } + + return closestColorName; + } catch (e) { + debugPrint('Error guessing color name: $e'); + return ''; + } + } + + /// Converts the current state to a hexadecimal color string. + /// + /// If [includeAlpha] is true, the alpha channel will be included in the string. + String toHexString(bool includeAlpha) { + final colorValue = + toColor().value.toRadixString(16).padLeft(8, '0').toUpperCase(); + return includeAlpha ? '#$colorValue' : '#${colorValue.substring(2)}'; + } + + /// Clamps the color components to the specified ranges + /// + /// Updates the current HSV values to be within the given ranges: + /// - hue: between [minHue] and [maxHue] (0-360) + /// - saturation: between [minSaturation] and [maxSaturation] (0-1) + /// - value: between [minValue] and [maxValue] (0-1) + /// + /// RGB values are automatically recalculated when HSV values are clamped. + void clampToBounds({ + int minHue = 0, + int maxHue = 360, + int minSaturation = 0, + int maxSaturation = 100, + int minValue = 0, + int maxValue = 100, + }) { + assert(minHue >= 0 && minHue <= maxHue && maxHue <= 360, + 'Hue values must be between 0 and 360'); + assert( + minSaturation >= 0 && + minSaturation <= maxSaturation && + maxSaturation <= 100, + 'Saturation values must be between 0 and 100'); + assert(minValue >= 0 && minValue <= maxValue && maxValue <= 100, + 'Value/brightness values must be between 0 and 100'); + + // Clamp values to allowed ranges + final clampedHue = _hue.clamp(minHue.toDouble(), maxHue.toDouble()); + final clampedSaturation = + _saturation.clamp(minSaturation / 100, maxSaturation / 100); + final clampedValue = _value.clamp(minValue / 100, maxValue / 100); + + // Only update and recalculate if values actually changed + if (clampedHue != _hue || + clampedSaturation != _saturation || + clampedValue != _value) { + _hue = clampedHue; + _saturation = clampedSaturation; + _value = clampedValue; + _recalculateRGBFromHSV(); + } + } + + /// Updates the HSV values based on the current RGB values. + void _recalculateHSVFromRGB() { + if (_red == 0 && _green == 0 && _blue == 0) { + _value = 0; + return; + } + + final v = math.max(_red, math.max(_green, _blue)); + if (v > 0) { + final scaledR = _red / v; + final scaledG = _green / v; + final scaledB = _blue / v; + + final (h, s, tempV) = rgbToHsv(scaledR, scaledG, scaledB); + _hue = h; + _saturation = s; + } + _value = v; + } + + /// Updates the RGB values based on the current HSV values. + void _recalculateRGBFromHSV() { + final (r, g, b) = hsvToRgb(_hue, _saturation, _value); + _red = r; + _green = g; + _blue = b; + } + + /// Validates that all color values are within their valid ranges. + void _validateColorValues() { + assert(_red >= 0 && _red <= 1, "Red must be between 0 and 1"); + assert(_green >= 0 && _green <= 1, "Green must be between 0 and 1"); + assert(_blue >= 0 && _blue <= 1, "Blue must be between 0 and 1"); + assert(_alpha >= 0 && _alpha <= 1, "Alpha must be between 0 and 1"); + assert(_hue >= 0 && _hue <= 360, "Hue must be between 0 and 360"); + assert(_saturation >= 0 && _saturation <= 1, + "Saturation must be between 0 and 1"); + assert(_value >= 0 && _value <= 1, "Value must be between 0 and 1"); + } + + /// Creates a copy of this [ColorState] but with the given fields replaced with the new values. + ColorState copyWith({ + double? red, + double? green, + double? blue, + double? alpha, + double? hue, + double? saturation, + double? value, + }) { + ColorState cs = ColorState( + _red, + _green, + _blue, + _alpha, + _hue, + _saturation, + _value, + ); + + if (red != null && red != cs._red) cs.setRed(red); + if (green != null && green != cs._green) cs.setGreen(green); + if (blue != null && blue != cs._blue) cs.setBlue(blue); + if (alpha != null && alpha != cs._alpha) cs.setAlpha(alpha); + if (hue != null && hue != cs._hue) cs.setHue(hue); + if (saturation != null && saturation != cs._saturation) { + cs.setSaturation(saturation); + } + if (value != null && value != cs._value) cs.setValue(value); + + return cs; + } + + /// Converts Color to HSV. + static (double h, double s, double l) colorToHsv(Color color) { + final red = color.red / 255; + final green = color.green / 255; + final blue = color.blue / 255; + + return rgbToHsv(red, green, blue); + } + + /// Converts Color to HSL. + static (double h, double s, double l) colorToHsl(Color color) { + final red = color.red / 255; + final green = color.green / 255; + final blue = color.blue / 255; + + return rgbToHsl(red, green, blue); + } + + /// Converts RGB values to HSV. + static (double h, double s, double v) rgbToHsv(double r, double g, double b) { + final min = math.min(r, math.min(g, b)); + final max = math.max(r, math.max(g, b)); + final delta = max - min; + + final v = max; + final s = max == 0 ? 0.0 : delta / max; + + if (delta == 0) { + return (0, s, v); + } + + double h; + if (max == r) { + h = (g - b) / delta; + } else if (max == g) { + h = 2 + (b - r) / delta; + } else { + h = 4 + (r - g) / delta; + } + h *= 60; + if (h < 0) h += 360; + + return (h, s, v); + } + + /// Converts RGB values to HSL. + static (double h, double s, double l) rgbToHsl(double r, double g, double b) { + final max = math.max(r, math.max(g, b)); + final min = math.min(r, math.min(g, b)); + final delta = max - min; + + // Calculate lightness + final l = (max + min) / 2.0; + + // Calculate saturation + final s = delta == 0 ? 0.0 : delta / (1.0 - (2 * l - 1).abs()); + + // Calculate hue + double h; + if (delta == 0) { + h = 0.0; // achromatic (gray) + } else { + if (max == r) { + h = ((g - b) / delta) % 6; + } else if (max == g) { + h = (b - r) / delta + 2.0; + } else { + // max == b + h = (r - g) / delta + 4.0; + } + h *= 60; + if (h < 0) h += 360; + } + + return (h, s, l); + } + + /// Converts HSV values to RGB. + static (double r, double g, double b) hsvToRgb(double h, double s, double v) { + if (s <= 0) return (v, v, v); // achromatic (grey) + + h = (h % 360) / 60; // Normalize hue + final i = h.floor(); + final f = h - i.toDouble(); + final p = v * (1.0 - s); + final q = v * (1.0 - s * f); + final t = v * (1.0 - s * (1.0 - f)); + + switch (i % 6) { + case 0: // 0 <= h < 60 + return (v, t, p); + case 1: + return (q, v, p); + case 2: + return (p, v, t); + case 3: + return (p, q, v); + case 4: + return (t, p, v); + default: + return (v, p, q); + } + } + + /// Converts HSL values to RGB. + static (double r, double g, double b) hslToRgb(double h, double s, double l) { + if (s == 0) return (l, l, l); // achromatic (grey) + + final q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + final p = 2.0 * l - q; + final r = _hueToRgb(p, q, h + 1.0 / 3.0); + final g = _hueToRgb(p, q, h); + final b = _hueToRgb(p, q, h - 1.0 / 3.0); + + return (r, g, b); + } + + /// Converts HSV values to HSL. + static (double h, double s, double l) hsvToHsl(double h, double s, double v) { + final hslH = h; + final hslL = v - v * s / 2.0; + final hslS = + (hslL == 0 || hslL == 1) ? 0.0 : (v - hslL) / math.min(hslL, 1 - hslL); + return (hslH, hslS, hslL); + } + + /// Converts HSL values to HSV. + static (double h, double s, double v) hslToHsv(double h, double s, double l) { + final hsvH = h; + final hsvV = l + s * math.min(l, 1 - l); + final hsvS = hsvV == 0 ? 0.0 : 2 * (1 - l / hsvV); + return (hsvH, hsvS, hsvV); + } + + /// Helper method for HSL to RGB conversion. + static double _hueToRgb(double p, double q, double t) { + if (t < 0.0) { + t += 1.0; + } else if (t > 1.0) { + t -= 1.0; + } + + if (t < 1.0 / 6.0) { + return p + (q - p) * 6.0 * t; + } else if (t < 1.0 / 2.0) { + return q; + } else if (t < 2.0 / 3.0) { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + } else { + return p; + } + } + + /// Get distance between two colors considering both RGB and HSL spaces + static double colorDistance(Color from, Color to) { + // Normalize RGB values to range 0-1 + final fromRgb = + (red: from.red / 255, green: from.green / 255, blue: from.blue / 255); + final toRgb = + (red: to.red / 255, green: to.green / 255, blue: to.blue / 255); + + // Convert RGB to HSL + final fromHsl = rgbToHsl(fromRgb.red, fromRgb.green, fromRgb.blue); + final toHsl = rgbToHsl(toRgb.red, toRgb.green, toRgb.blue); + + // Calculate distance using _distanceBetween + return _rgbHslDistance(fromRgb, fromHsl, toRgb, toHsl); + } + + /// Get distance between two ColorState objects considering both RGB and HSL spaces + static double colorStateDistance(ColorState from, ColorState to) { + // Extract RGB values from ColorState objects + final fromRgb = (red: from.red, green: from.green, blue: from.blue); + final toRgb = (red: to.red, green: to.green, blue: to.blue); + + // Extract HSL values from ColorState objects + final fromHsl = rgbToHsl(from.red, from.green, from.blue); + final toHsl = rgbToHsl(to.red, to.green, to.blue); + + // Calculate distance using _distanceBetween method + return _rgbHslDistance(fromRgb, fromHsl, toRgb, toHsl); + } + + /// Distance calculation between two colors in RGB and HSL spaces. + static double _rgbHslDistance( + ({double red, double green, double blue}) rgb1, + (double h, double s, double l) hsl1, + ({double red, double green, double blue}) rgb2, + (double h, double s, double l) hsl2) { + // RGB distance = (R1 - R2)^2 + (G1 - G2)^2 + (B1 - B2)^2 + final rgbDiff = math.pow((rgb1.red - rgb2.red) * 255, 2) + + math.pow((rgb1.green - rgb2.green) * 255, 2) + + math.pow((rgb1.blue - rgb2.blue) * 255, 2); + + // HSL distance = ((H1 - H2)/360)^2 + (S1 - S2)^2 + (L1 - L2)^2 + final hslDiff = math.pow((hsl1.$1 - hsl2.$1) / 360, 2) + + math.pow(hsl1.$2 - hsl2.$2, 2) + + math.pow(hsl1.$3 - hsl2.$3, 2); + + return rgbDiff + (hslDiff * 2); + } +} diff --git a/test/color_picker_test.dart b/test/color_picker_test.dart new file mode 100644 index 000000000..d2e0399ad --- /dev/null +++ b/test/color_picker_test.dart @@ -0,0 +1,147 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:fluent_ui/src/controls/form/color_picker/color_spectrum.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'app_test.dart'; + +void main() { + testWidgets( + 'ColorPicker - verifies initial state and preview visibility', + (WidgetTester tester) async { + Color currentColor = Colors.blue; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return wrapApp( + child: ColorPicker( + color: currentColor, + onChanged: (Color color) { + setState(() { + currentColor = color; + }); + }, + ), + ); + }, + ), + ); + + // Verify the ColorPicker widget exists + expect(find.byType(ColorPicker), findsOneWidget); + + // Check if initial color is preserved + final initialColorState = currentColor; + expect(initialColorState, Colors.blue); + + // Find color preview container + final preview = find.descendant( + of: find.byType(ColorPicker), + matching: find.byWidgetPredicate((widget) => + widget is Container && + widget.decoration != null && + widget.decoration is BoxDecoration), + ); + expect(preview, findsWidgets); + + await tester.pump(const Duration(seconds: 1)); + + // Verify the color value matches initial blue color + expect(currentColor.value, equals(Colors.blue.value), + reason: 'Color value should match initial blue color'); + }, + ); + + testWidgets( + 'ColorPicker - switches between ring and box spectrum shapes', + (WidgetTester tester) async { + Color currentColor = Colors.blue; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return wrapApp( + child: ColorPicker( + color: currentColor, + onChanged: (Color color) { + setState(() { + currentColor = color; + }); + }, + colorSpectrumShape: ColorSpectrumShape.box, + ), + ); + }, + ), + ); + + // Verify correct spectrum type is rendered + expect(find.byType(ColorBoxSpectrum), findsOneWidget, + reason: 'Box spectrum should be visible when specified'); + expect(find.byType(ColorRingSpectrum), findsNothing, + reason: 'Ring spectrum should not be visible when box is specified'); + + // Find and interact with box spectrum + final boxSpectrum = find.byType(ColorBoxSpectrum); + await tester.tapAt(tester.getCenter(boxSpectrum)); + await tester.pumpAndSettle(); + + // Verify color changed after interaction + expect(currentColor, isNot(equals(Colors.blue)), + reason: 'Color should change after spectrum interaction'); + }, + ); + + testWidgets( + 'ColorPicker - changes color through hex input with alpha support', + (WidgetTester tester) async { + Color currentColor = Colors.blue; + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return wrapApp( + child: ColorPicker( + color: currentColor, + onChanged: (Color color) { + setState(() { + currentColor = color; + }); + }, + isColorSliderVisible: false, + isAlphaSliderVisible: false, + isAlphaTextInputVisible: false, + isMoreButtonVisible: false, + isColorChannelTextInputVisible: false, + isHexInputVisible: true, + ), + ); + }, + ), + ); + + // Wait for widget to settle + await tester.pump(); + await tester.pumpAndSettle(); + + // Find hex input field + final textboxFinder = find.byType(TextBox); + expect(textboxFinder, findsWidgets, + reason: 'Hex input TextBox should be visible'); + + // Test fully opaque red color + await tester.enterText(textboxFinder.first, '#FFFF0000'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + expect(currentColor, equals(const Color(0xFFFF0000)), + reason: 'Color should change to opaque red'); + + // Test semi-transparent red color + await tester.enterText(textboxFinder.first, '#80FF0000'); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + expect(currentColor, equals(const Color(0x80FF0000)), + reason: 'Color should change to semi-transparent red'); + }, + ); +} From c0490cb92b21aee05b431171414c28d9d2a96c4d Mon Sep 17 00:00:00 2001 From: Jong Hyun Kim Date: Wed, 11 Dec 2024 10:23:57 +0900 Subject: [PATCH 2/3] refactor: Improve ColorPicker code organization and structure - Convert ColorPickerSizes and ColorPickerSpacing to private enums - Simplify debugFillProperties and checkerboard pattern - Fix ComboBox expansion and preview box layering - Adjust CheckerboardPainter implementation - Improve code readability by converting ternary operators to if statements Co-Authored-By: Bruno D'Luka <45696119+bdlukaa@users.noreply.github.com> --- .../form/color_picker/color_picker.dart | 410 ++++++++++-------- .../form/color_picker/color_spectrum.dart | 45 +- 2 files changed, 242 insertions(+), 213 deletions(-) diff --git a/lib/src/controls/form/color_picker/color_picker.dart b/lib/src/controls/form/color_picker/color_picker.dart index d2a63027c..f7c74032b 100644 --- a/lib/src/controls/form/color_picker/color_picker.dart +++ b/lib/src/controls/form/color_picker/color_picker.dart @@ -26,30 +26,39 @@ enum ColorMode { } /// Color picker spacing constants -class ColorPickerSpacing { +enum _ColorPickerSpacing { /// Small spacing between widgets - static const double small = 12.0; + small(12.0), /// Large spacing between widgets - static const double large = 24.0; + large(24.0); + + final double size; + + const _ColorPickerSpacing(this.size); } /// Color picker component sizing constants -class ColorPickerSizes { +enum _ColorPickerSizes { /// The size of the color spectrum widget - static const double spectrum = 336.0; + spectrum(256.0), /// The width of the color preview box - static const double preview = 44.0; + preview(44.0), /// The height of the sliders - static const double slider = 12.0; + slider(12.0), /// The width of the input boxes - static const double inputBox = 120.0; + inputBox(120.0); + + final double size; - /// The maximum width of the color spectrum and preview box - static double maxWidth = spectrum + ColorPickerSpacing.small + preview; // 392 + const _ColorPickerSizes(this.size); + + /// Define the computed maxWidth as a static constant + static double get maxWidth => + spectrum.size + _ColorPickerSpacing.small.size + preview.size; } /// Color Picker @@ -190,66 +199,65 @@ class ColorPicker extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(ColorProperty('color', color)); - properties.add(EnumProperty('orientation', orientation)); - properties.add(EnumProperty( - 'colorSpectrumShape', colorSpectrumShape)); - properties.add(FlagProperty( - 'isColorPreviewVisible', - value: isColorPreviewVisible, - defaultValue: true, - ifFalse: 'color preview hidden', - )); - properties.add(FlagProperty( - 'isColorSliderVisible', - value: isColorSliderVisible, - defaultValue: true, - ifFalse: 'color slider hidden', - )); - properties.add(FlagProperty( - 'isMoreButtonVisible', - value: isMoreButtonVisible, - defaultValue: true, - ifFalse: 'more button hidden', - )); - properties.add(FlagProperty( - 'isHexInputVisible', - value: isHexInputVisible, - defaultValue: true, - ifFalse: 'hex input hidden', - )); - properties.add(FlagProperty( - 'isColorChannelTextInputVisible', - value: isColorChannelTextInputVisible, - defaultValue: true, - ifFalse: 'color channel text input hidden', - )); - properties.add(FlagProperty( - 'isAlphaEnabled', - value: isAlphaEnabled, - defaultValue: true, - ifFalse: 'alpha disabled', - )); - properties.add(FlagProperty( - 'isAlphaSliderVisible', - value: isAlphaSliderVisible, - defaultValue: true, - ifFalse: 'alpha slider hidden', - )); - properties.add(FlagProperty( - 'isAlphaTextInputVisible', - value: isAlphaTextInputVisible, - defaultValue: true, - ifFalse: 'alpha text input hidden', - )); - properties.add(IntProperty('minHue', minHue, defaultValue: 0)); - properties.add(IntProperty('maxHue', maxHue, defaultValue: 359)); - properties - .add(IntProperty('minSaturation', minSaturation, defaultValue: 0)); properties - .add(IntProperty('maxSaturation', maxSaturation, defaultValue: 100)); - properties.add(IntProperty('minValue', minValue, defaultValue: 0)); - properties.add(IntProperty('maxValue', maxValue, defaultValue: 100)); + ..add(ColorProperty('color', color)) + ..add(EnumProperty('orientation', orientation)) + ..add(EnumProperty( + 'colorSpectrumShape', colorSpectrumShape)) + ..add(FlagProperty( + 'isColorPreviewVisible', + value: isColorPreviewVisible, + defaultValue: true, + ifFalse: 'color preview hidden', + )) + ..add(FlagProperty( + 'isColorSliderVisible', + value: isColorSliderVisible, + defaultValue: true, + ifFalse: 'color slider hidden', + )) + ..add(FlagProperty( + 'isMoreButtonVisible', + value: isMoreButtonVisible, + defaultValue: true, + ifFalse: 'more button hidden', + )) + ..add(FlagProperty( + 'isHexInputVisible', + value: isHexInputVisible, + defaultValue: true, + ifFalse: 'hex input hidden', + )) + ..add(FlagProperty( + 'isColorChannelTextInputVisible', + value: isColorChannelTextInputVisible, + defaultValue: true, + ifFalse: 'color channel text input hidden', + )) + ..add(FlagProperty( + 'isAlphaEnabled', + value: isAlphaEnabled, + defaultValue: true, + ifFalse: 'alpha disabled', + )) + ..add(FlagProperty( + 'isAlphaSliderVisible', + value: isAlphaSliderVisible, + defaultValue: true, + ifFalse: 'alpha slider hidden', + )) + ..add(FlagProperty( + 'isAlphaTextInputVisible', + value: isAlphaTextInputVisible, + defaultValue: true, + ifFalse: 'alpha text input hidden', + )) + ..add(IntProperty('minHue', minHue, defaultValue: 0)) + ..add(IntProperty('maxHue', maxHue, defaultValue: 359)) + ..add(IntProperty('minSaturation', minSaturation, defaultValue: 0)) + ..add(IntProperty('maxSaturation', maxSaturation, defaultValue: 100)) + ..add(IntProperty('minValue', minValue, defaultValue: 0)) + ..add(IntProperty('maxValue', maxValue, defaultValue: 100)); } } @@ -301,45 +309,47 @@ class _ColorPickerState extends State { (widget.isAlphaEnabled && widget.isAlphaTextInputVisible); // Build the color picker layout based on orientation - return widget.orientation == Axis.vertical - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildSpectrumAndPreview(), - if (widget.isColorSliderVisible || - (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ - const SizedBox(height: ColorPickerSpacing.large), - _buildSliders(), - ], - if (widget.isMoreButtonVisible && hasVisibleInputs) ...[ - const SizedBox(height: ColorPickerSpacing.large), - _buildMoreButton(), - ], - if (!widget.isMoreButtonVisible || _isMoreExpanded) ...[ - if (hasVisibleInputs) ...[ - const SizedBox(height: ColorPickerSpacing.large), - _buildInputs(), - ], - ], - ], - ) - : Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - _buildSpectrumAndPreview(), - if (widget.isColorSliderVisible || - (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ - const SizedBox(width: ColorPickerSpacing.large), - _buildSliders(), - ], - if (hasVisibleInputs) ...[ - const SizedBox(width: ColorPickerSpacing.large), - _buildInputs(), - ], + if (widget.orientation == Axis.vertical) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSpectrumAndPreview(), + if (widget.isColorSliderVisible || + (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ + SizedBox(height: _ColorPickerSpacing.large.size), + _buildSliders(), + ], + if (widget.isMoreButtonVisible && hasVisibleInputs) ...[ + SizedBox(height: _ColorPickerSpacing.large.size), + _buildMoreButton(), + ], + if (!widget.isMoreButtonVisible || _isMoreExpanded) ...[ + if (hasVisibleInputs) ...[ + SizedBox(height: _ColorPickerSpacing.large.size), + _buildInputs(), ], - ); + ], + ], + ); + } else { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildSpectrumAndPreview(), + if (widget.isColorSliderVisible || + (widget.isAlphaEnabled && widget.isAlphaSliderVisible)) ...[ + SizedBox(width: _ColorPickerSpacing.large.size), + _buildSliders(), + ], + if (hasVisibleInputs) ...[ + SizedBox(width: _ColorPickerSpacing.large.size), + _buildInputs(), + ], + ], + ); + } } /// Initializes the text controllers and focus nodes. @@ -390,12 +400,13 @@ class _ColorPickerState extends State { /// Builds the "More" button to expand the color picker inputs. Widget _buildMoreButton() { final moreButton = SizedBox( - width: ColorPickerSizes.inputBox, + width: _ColorPickerSizes.inputBox.size, child: Button( onPressed: () => setState(() => _isMoreExpanded = !_isMoreExpanded), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + // TODO: Localize for 'Less' and 'More' later Text(_isMoreExpanded ? 'Less' : 'More'), Icon( _isMoreExpanded @@ -408,7 +419,7 @@ class _ColorPickerState extends State { )); return SizedBox( - width: ColorPickerSizes.maxWidth, + width: _ColorPickerSizes.maxWidth, child: Align( alignment: Alignment.centerRight, child: moreButton, @@ -519,14 +530,14 @@ class _ColorSpectrumAndPreview extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( width: (orientation == Axis.horizontal) && !isColorPreviewVisible - ? ColorPickerSizes.spectrum - : ColorPickerSizes.maxWidth, - height: ColorPickerSizes.spectrum, + ? _ColorPickerSizes.spectrum.size + : _ColorPickerSizes.maxWidth, + height: _ColorPickerSizes.spectrum.size, child: isColorPreviewVisible ? Row( children: [ _buildSpectrum(), - const SizedBox(width: ColorPickerSpacing.small), + SizedBox(width: _ColorPickerSpacing.small.size), _buildPreviewBox(context), ], ) @@ -536,8 +547,8 @@ class _ColorSpectrumAndPreview extends StatelessWidget { Widget _buildSpectrum() { return SizedBox( - width: ColorPickerSizes.spectrum, - height: ColorPickerSizes.spectrum, + width: _ColorPickerSizes.spectrum.size, + height: _ColorPickerSizes.spectrum.size, child: colorSpectrumShape == ColorSpectrumShape.ring ? ColorRingSpectrum( colorState: colorState, @@ -559,38 +570,50 @@ class _ColorSpectrumAndPreview extends StatelessWidget { } Widget _buildPreviewBox(BuildContext context) { - const double width = ColorPickerSizes.preview; - const double height = ColorPickerSizes.spectrum; + double width = _ColorPickerSizes.preview.size; + double height = _ColorPickerSizes.spectrum.size; const double borderRadius = 4.0; + const double borderWidth = 2.0; final theme = FluentTheme.of(context); final color = colorState.toColor(); - return Container( + return SizedBox( width: width, height: height, - decoration: BoxDecoration( - border: Border.all( - color: theme.resources.dividerStrokeColorDefault, - ), - borderRadius: BorderRadius.circular(borderRadius), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(borderRadius - 1), - child: Stack( - children: [ - Positioned.fill( + child: Stack( + children: [ + // Base checkerboard pattern + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), child: CustomPaint( - painter: CheckerboardPainter(), + painter: CheckerboardPainter( + theme: theme, + ), ), ), - Positioned.fill( + ), + // Color overlay + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), child: Container( color: color, ), ), - ], - ), + ), + // Border on top + Container( + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.dividerStrokeColorDefault, + width: borderWidth, + ), + borderRadius: BorderRadius.circular(borderRadius), + ), + ), + ], ), ); } @@ -636,20 +659,21 @@ class _ColorSliders extends StatelessWidget { @override Widget build(BuildContext context) { - final thumbColor = FluentTheme.of(context).resources.focusStrokeColorOuter; + final theme = FluentTheme.of(context); + // Determine if the sliders should be displayed horizontally or vertically final bool isVertical = orientation != Axis.vertical; final sliders = [ - if (isColorSliderVisible) _buildValueSlider(thumbColor, isVertical), + if (isColorSliderVisible) _buildValueSlider(theme, isVertical), if (isColorSliderVisible && isAlphaSliderVisible && isAlphaEnabled) orientation == Axis.vertical - ? const SizedBox(height: ColorPickerSpacing.large) - : const SizedBox( - width: ColorPickerSpacing.large, + ? SizedBox(height: _ColorPickerSpacing.large.size) + : SizedBox( + width: _ColorPickerSpacing.large.size, ), if (isAlphaSliderVisible && isAlphaEnabled) - _buildAlphaSlider(thumbColor, isVertical), + _buildAlphaSlider(theme, isVertical), ]; return orientation == Axis.horizontal @@ -664,14 +688,19 @@ class _ColorSliders extends StatelessWidget { } /// Builds the value slider for the color picker. - Widget _buildValueSlider(Color thumbColor, bool isVertical) { + Widget _buildValueSlider(FluentThemeData theme, bool isVertical) { + final thumbColor = theme.resources.focusStrokeColorOuter; final colorName = colorState.guessColorName(); final valueText = '${(colorState.value * 100).round()}% ${colorName.isNotEmpty ? "($colorName)" : ""}'; return SizedBox( - width: isVertical ? ColorPickerSizes.slider : ColorPickerSizes.maxWidth, - height: isVertical ? ColorPickerSizes.spectrum : ColorPickerSizes.slider, + width: isVertical + ? _ColorPickerSizes.slider.size + : _ColorPickerSizes.maxWidth, + height: isVertical + ? _ColorPickerSizes.spectrum.size + : _ColorPickerSizes.slider.size, child: ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( @@ -714,22 +743,26 @@ class _ColorSliders extends StatelessWidget { } /// Builds the alpha slider for the color picker. - Widget _buildAlphaSlider(Color thumbColor, bool isVertical) { - final opacityText = '${(colorState.alpha * 100).round()}% opacity'; + Widget _buildAlphaSlider(FluentThemeData theme, bool isVertical) { + final thumbColor = theme.resources.focusStrokeColorOuter; + final opacityText = + '${(colorState.alpha * 100).round()}% opacity'; // TODO: Localize return SizedBox( width: isVertical - ? ColorPickerSizes.slider - : ColorPickerSizes.spectrum + - ColorPickerSpacing.small + - ColorPickerSizes.preview, - height: isVertical ? ColorPickerSizes.spectrum : ColorPickerSizes.slider, + ? _ColorPickerSizes.slider.size + : _ColorPickerSizes.spectrum.size + + _ColorPickerSpacing.small.size + + _ColorPickerSizes.preview.size, + height: isVertical + ? _ColorPickerSizes.spectrum.size + : _ColorPickerSizes.slider.size, child: ClipRRect( borderRadius: BorderRadius.circular(6), child: Stack( children: [ Positioned.fill( - child: CustomPaint(painter: CheckerboardPainter()), + child: CustomPaint(painter: CheckerboardPainter(theme: theme)), ), Container( decoration: BoxDecoration( @@ -871,11 +904,11 @@ class _ColorInputs extends StatelessWidget { return orientation == Axis.vertical ? SizedBox( - width: ColorPickerSizes.maxWidth, + width: _ColorPickerSizes.maxWidth, child: inputsContent, ) : SizedBox( - height: ColorPickerSizes.spectrum, + height: _ColorPickerSizes.spectrum.size, width: 200, // arbitrary width, but more than enough child: inputsContent, ); @@ -900,12 +933,13 @@ class _ColorInputs extends StatelessWidget { /// Builds the color mode selector and hex input. Widget _buildColorModeAndHexInput(ColorMode colorMode) { final modeSelector = SizedBox( - width: ColorPickerSizes.inputBox, + width: _ColorPickerSizes.inputBox.size, child: ComboBox( value: colorMode, items: colorModes.entries .map((e) => ComboBoxItem(value: e.value, child: Text(e.key))) .toList(), + isExpanded: true, onChanged: (value) { if (value != null) _colorModeNotifier.value = value; }, @@ -913,44 +947,46 @@ class _ColorInputs extends StatelessWidget { ); final hexInput = SizedBox( - width: ColorPickerSizes.inputBox, + width: _ColorPickerSizes.inputBox.size, child: TextBox( controller: hexController, placeholder: isAlphaEnabled ? '#AARRGGBB' : '#RRGGBB', onSubmitted: _updateHexColor, )); - return orientation == Axis.vertical - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (isColorChannelTextInputVisible) ...[ - modeSelector, - ], - if (isHexInputVisible) ...[ - hexInput, - ], - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isHexInputVisible) ...[ - Align( - alignment: Alignment.centerLeft, - child: hexInput, - ), - const SizedBox(height: ColorPickerSpacing.small), - ], - if (isColorChannelTextInputVisible) ...[ - Align( - alignment: Alignment.centerLeft, - child: modeSelector, - ), - ], - ], - ); + if (orientation == Axis.vertical) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (isColorChannelTextInputVisible) ...[ + modeSelector, + ], + if (isHexInputVisible) ...[ + hexInput, + ], + ], + ); + } else { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isHexInputVisible) ...[ + Align( + alignment: Alignment.centerLeft, + child: hexInput, + ), + SizedBox(height: _ColorPickerSpacing.small.size), + ], + if (isColorChannelTextInputVisible) ...[ + Align( + alignment: Alignment.centerLeft, + child: modeSelector, + ), + ], + ], + ); + } } /// Builds the RGB input fields. @@ -1068,12 +1104,12 @@ class _ColorInputs extends StatelessWidget { }) { // TODO: initial format issue of NumberBox not being applied. return Column(children: [ - const SizedBox(height: ColorPickerSpacing.small), + SizedBox(height: _ColorPickerSpacing.small.size), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( - width: ColorPickerSizes.inputBox, + width: _ColorPickerSizes.inputBox.size, child: NumberBox( value: value, min: min, diff --git a/lib/src/controls/form/color_picker/color_spectrum.dart b/lib/src/controls/form/color_picker/color_spectrum.dart index 603366bcb..462c4a7cd 100644 --- a/lib/src/controls/form/color_picker/color_spectrum.dart +++ b/lib/src/controls/form/color_picker/color_spectrum.dart @@ -714,43 +714,36 @@ class _BoxSpectrumPainter extends CustomPainter { /// /// This painter is used to represent transparency in the [ColorPicker]'s alpha slider. class CheckerboardPainter extends CustomPainter { + /// The theme data for styling + final FluentThemeData theme; + + const CheckerboardPainter({ + required this.theme, + }); + @override void paint(Canvas canvas, Size size) { - const squareSize = 5; + const squareSize = 4; // GCF of Slider width(12) and Preview width(44) final paint = Paint(); final width = size.width.toInt(); final height = size.height.toInt(); - for (int i = 0; i < width; i += (squareSize * 2)) { - for (int j = 0; j < height; j += (squareSize * 2)) { - // Draw dark squares - paint.color = const Color(0xFFD3D3D3); // Light gray + for (int i = 0; i < width; i += squareSize) { + for (int j = 0; j < height; j += squareSize) { + // Determine if this position should be a dark square + final isDarkSquare = (i ~/ squareSize + j ~/ squareSize) % 2 != 0; + + paint.color = isDarkSquare + ? theme.brightness.isDark + ? const Color(0x20D8D8D8) + : const Color(0x20393939) + : Colors.transparent; + canvas.drawRect( Rect.fromLTWH(i.toDouble(), j.toDouble(), squareSize.toDouble(), squareSize.toDouble()), paint, ); - canvas.drawRect( - Rect.fromLTWH( - (i + squareSize).toDouble(), - (j + squareSize).toDouble(), - squareSize.toDouble(), - squareSize.toDouble()), - paint, - ); - - // Draw light squares - paint.color = Colors.white; - canvas.drawRect( - Rect.fromLTWH((i + squareSize).toDouble(), j.toDouble(), - squareSize.toDouble(), squareSize.toDouble()), - paint, - ); - canvas.drawRect( - Rect.fromLTWH(i.toDouble(), (j + squareSize).toDouble(), - squareSize.toDouble(), squareSize.toDouble()), - paint, - ); } } } From 970a7e5f2a3e99edf21e21e7a3c41c2c8f6a4f26 Mon Sep 17 00:00:00 2001 From: Jong Hyun Kim Date: Wed, 11 Dec 2024 17:30:36 +0900 Subject: [PATCH 3/3] feat: Update ColorPicker to match WinUI standards - Update color conversion and brightness calculation - Implement WinUI-compliant value text format - Replace record typedefs with final classes - Update color names dictionary - Optimize CheckerboardPainter performance - Make ColorMode enum private - Update documentation and changelog Co-Authored-By: Bruno D'Luka <45696119+bdlukaa@users.noreply.github.com> --- CHANGELOG.md | 37 +- .../form/color_picker/color_names.dart | 1625 +---------------- .../form/color_picker/color_picker.dart | 89 +- .../form/color_picker/color_spectrum.dart | 65 +- .../form/color_picker/color_state.dart | 251 ++- 5 files changed, 324 insertions(+), 1743 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1106316f..e3ab2fbf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,6 @@ ## [next] -- feat: Add ColorPicker component following WinUI 3 specifications ([#1001](https://github.com/bdlukaa/fluent_ui/issues/1001) -- fix: Add missing properties (`closeIconSize`, `closeButtonStyle`) in `debugFillProperties` and `InfoBarThemeData.merge` ([#1128](https://github.com/bdlukaa/fluent_ui/issues/1128) -- feat: Add `TabView.reservedStripWidth`, which adds a minimum empty area between the tabs and the tab view footer ([#1106](https://github.com/bdlukaa/fluent_ui/issues/1106))] -- fix: Correctly unfocus `NumberBox` when user taps outside ([#1135](https://github.com/bdlukaa/fluent_ui/issues/1135)) -- fix: Do try to scroll Date and Time at build time ([#1117](https://github.com/bdlukaa/fluent_ui/issues/1117)) -- feat: Use a `Decoration` instead of `Color` in `NavigationAppBar` ([#1118](https://github.com/bdlukaa/fluent_ui/issues/1118)) -- feat: Add `EditableComboBox.inputFormatters` ([#1041](https://github.com/bdlukaa/fluent_ui/issues/1041)) -- **BREAKING** feat: `TextBox.decoration` and `TextBox.foregroundDecoration` are now of type `WidgetStateProperty` ([#987](https://github.com/bdlukaa/fluent_ui/pull/987)) - - Before: - ```dart - TextBox( - decoration: BoxDecoration( - color: Colors.red, - ), - foregroundDecoration: BoxDecoration( - color: Colors.blue, - ), - ), - ``` - - After: - ```dart - TextBox( - decoration: WidgetStateProperty.all(BoxDecoration( - color: Colors.red, - )), - foregroundDecoration: WidgetStateProperty.all(BoxDecoration( - color: Colors.blue, - )), - ), - ``` -- feat: Add `TabView.gestures`, which allows the manipulation of the tab gestures ([#1138](https://github.com/bdlukaa/fluent_ui/issues/1138)) -- feat: Add `DropDownButton.style` ([#1139](https://github.com/bdlukaa/fluent_ui/issues/1139)) -- feat: Possibility to open date and time pickers programatically ([#1142](https://github.com/bdlukaa/fluent_ui/issues/1142)) -- fix: `TimePicker` hour offset +- feat: Add `ColorPicker` ([#1001](https://github.com/bdlukaa/fluent_ui/issues/1001) - fix: Add missing properties (`closeIconSize`, `closeButtonStyle`) in `debugFillProperties` and `InfoBarThemeData.merge` ([#1128](https://github.com/bdlukaa/fluent_ui/issues/1128) - feat: Add `TabView.reservedStripWidth`, which adds a minimum empty area between the tabs and the tab view footer ([#1106](https://github.com/bdlukaa/fluent_ui/issues/1106))] - fix: Correctly unfocus `NumberBox` when user taps outside ([#1135](https://github.com/bdlukaa/fluent_ui/issues/1135)) diff --git a/lib/src/controls/form/color_picker/color_names.dart b/lib/src/controls/form/color_picker/color_names.dart index 9087fdac9..a40cdc5d3 100644 --- a/lib/src/controls/form/color_picker/color_names.dart +++ b/lib/src/controls/form/color_picker/color_names.dart @@ -1,1574 +1,147 @@ part of "color_state.dart"; -/// Contains color names adapted from "Name That Color" -/// See: http://chir.ag/projects/name-that-color +/// Contains color names from Windows.UI Colors +/// Extracted using System.Reflection to match WinUI standards +/// Total: 138 colors and transparent class _ColorNames { /// A map of color names and their corresponding hex values. static const Map _values = { 0xFF000000: "Black", - 0xFF000080: "Navy Blue", - 0xFF0000C8: "Dark Blue", + 0xFF000080: "Navy", + 0xFF00008B: "Dark Blue", + 0xFF0000CD: "Medium Blue", 0xFF0000FF: "Blue", - 0xFF000741: "Stratos", - 0xFF001B1C: "Swamp", - 0xFF002387: "Resolution Blue", - 0xFF002900: "Deep Fir", - 0xFF002E20: "Burnham", - 0xFF002FA7: "International Klein Blue", - 0xFF003153: "Prussian Blue", - 0xFF003366: "Midnight Blue", - 0xFF003399: "Smalt", - 0xFF003532: "Deep Teal", - 0xFF003E40: "Cyprus", - 0xFF004620: "Kaitoke Green", - 0xFF0047AB: "Cobalt", - 0xFF004816: "Crusoe", - 0xFF004950: "Sherpa Blue", - 0xFF0056A7: "Endeavour", - 0xFF00581A: "Camarone", - 0xFF0066CC: "Science Blue", - 0xFF0066FF: "Blue Ribbon", - 0xFF00755E: "Tropical Rain Forest", - 0xFF0076A3: "Allports", - 0xFF007BA7: "Deep Cerulean", - 0xFF007EC7: "Lochmara", - 0xFF007FFF: "Azure Radiance", + 0xFF006400: "Dark Green", + 0xFF008000: "Green", 0xFF008080: "Teal", - 0xFF0095B6: "Bondi Blue", - 0xFF009DC4: "Pacific Blue", - 0xFF00A693: "Persian Green", - 0xFF00A86B: "Jade", - 0xFF00CC99: "Caribbean Green", - 0xFF00CCCC: "Robin's Egg Blue", - 0xFF00FF00: "Green", + 0xFF008B8B: "Dark Cyan", + 0xFF00BFFF: "Deep Sky Blue", + 0xFF00CED1: "Dark Turquoise", + 0xFF00FA9A: "Medium Spring Green", + 0xFF00FF00: "Lime", 0xFF00FF7F: "Spring Green", - 0xFF00FFFF: "Cyan / Aqua", - 0xFF010D1A: "Blue Charcoal", - 0xFF011635: "Midnight", - 0xFF011D13: "Holly", - 0xFF012731: "Daintree", - 0xFF01361C: "Cardin Green", - 0xFF01371A: "County Green", - 0xFF013E62: "Astronaut Blue", - 0xFF013F6A: "Regal Blue", - 0xFF014B43: "Aqua Deep", - 0xFF015E85: "Orient", - 0xFF016162: "Blue Stone", - 0xFF016D39: "Fun Green", - 0xFF01796F: "Pine Green", - 0xFF017987: "Blue Lagoon", - 0xFF01826B: "Deep Sea", - 0xFF01A368: "Green Haze", - 0xFF022D15: "English Holly", - 0xFF02402C: "Sherwood Green", - 0xFF02478E: "Congress Blue", - 0xFF024E46: "Evening Sea", - 0xFF026395: "Bahama Blue", - 0xFF02866F: "Observatory", - 0xFF02A4D3: "Cerulean", - 0xFF03163C: "Tangaroa", - 0xFF032B52: "Green Vogue", - 0xFF036A6E: "Mosque", - 0xFF041004: "Midnight Moss", - 0xFF041322: "Black Pearl", - 0xFF042E4C: "Blue Whale", - 0xFF044022: "Zuccini", - 0xFF044259: "Teal Blue", - 0xFF051040: "Deep Cove", - 0xFF051657: "Gulf Blue", - 0xFF055989: "Venice Blue", - 0xFF056F57: "Watercourse", - 0xFF062A78: "Catalina Blue", - 0xFF063537: "Tiber", - 0xFF069B81: "Gossamer", - 0xFF06A189: "Niagara", - 0xFF073A50: "Tarawera", - 0xFF080110: "Jaguar", - 0xFF081910: "Black Bean", - 0xFF082567: "Deep Sapphire", - 0xFF088370: "Elf Green", - 0xFF08E8DE: "Bright Turquoise", - 0xFF092256: "Downriver", - 0xFF09230F: "Palm Green", - 0xFF09255D: "Madison", - 0xFF093624: "Bottle Green", - 0xFF095859: "Deep Sea Green", - 0xFF097F4B: "Salem", - 0xFF0A001C: "Black Russian", - 0xFF0A480D: "Dark Fern", - 0xFF0A6906: "Japanese Laurel", - 0xFF0A6F75: "Atoll", - 0xFF0B0B0B: "Cod Gray", - 0xFF0B0F08: "Marshland", - 0xFF0B1107: "Gordons Green", - 0xFF0B1304: "Black Forest", - 0xFF0B6207: "San Felix", - 0xFF0BDA51: "Malachite", - 0xFF0C0B1D: "Ebony", - 0xFF0C0D0F: "Woodsmoke", - 0xFF0C1911: "Racing Green", - 0xFF0C7A79: "Surfie Green", - 0xFF0C8990: "Blue Chill", - 0xFF0D0332: "Black Rock", - 0xFF0D1117: "Bunker", - 0xFF0D1C19: "Aztec", - 0xFF0D2E1C: "Bush", - 0xFF0E0E18: "Cinder", - 0xFF0E2A30: "Firefly", - 0xFF0F2D9E: "Torea Bay", - 0xFF10121D: "Vulcan", - 0xFF101405: "Green Waterloo", - 0xFF105852: "Eden", - 0xFF110C6C: "Arapawa", - 0xFF120A8F: "Ultramarine", - 0xFF123447: "Elephant", - 0xFF126B40: "Jewel", - 0xFF130000: "Diesel", - 0xFF130A06: "Asphalt", - 0xFF13264D: "Blue Zodiac", - 0xFF134F19: "Parsley", - 0xFF140600: "Nero", - 0xFF1450AA: "Tory Blue", - 0xFF151F4C: "Bunting", - 0xFF1560BD: "Denim", - 0xFF15736B: "Genoa", - 0xFF161928: "Mirage", - 0xFF161D10: "Hunter Green", - 0xFF162A40: "Big Stone", - 0xFF163222: "Celtic", - 0xFF16322C: "Timber Green", - 0xFF163531: "Gable Green", - 0xFF171F04: "Pine Tree", - 0xFF175579: "Chathams Blue", - 0xFF182D09: "Deep Forest Green", - 0xFF18587A: "Blumine", - 0xFF19330E: "Palm Leaf", - 0xFF193751: "Nile Blue", - 0xFF1959A8: "Fun Blue", - 0xFF1A1A68: "Lucky Point", - 0xFF1AB385: "Mountain Meadow", - 0xFF1B0245: "Tolopea", - 0xFF1B1035: "Haiti", - 0xFF1B127B: "Deep Koamaru", - 0xFF1B1404: "Acadia", - 0xFF1B2F11: "Seaweed", - 0xFF1B3162: "Biscay", - 0xFF1B659D: "Matisse", - 0xFF1C1208: "Crowshead", - 0xFF1C1E13: "Rangoon Green", - 0xFF1C39BB: "Persian Blue", - 0xFF1C402E: "Everglade", - 0xFF1C7C7D: "Elm", - 0xFF1D6142: "Green Pea", - 0xFF1E0F04: "Creole", - 0xFF1E1609: "Karaka", - 0xFF1E1708: "El Paso", - 0xFF1E385B: "Cello", - 0xFF1E433C: "Te Papa Green", + 0xFF00FFFF: "Cyan", + 0xFF191970: "Midnight Blue", 0xFF1E90FF: "Dodger Blue", - 0xFF1E9AB0: "Eastern Blue", - 0xFF1F120F: "Night Rider", - 0xFF1FC2C2: "Java", - 0xFF20208D: "Jacksons Purple", - 0xFF202E54: "Cloud Burst", - 0xFF204852: "Blue Dianne", - 0xFF211A0E: "Eternity", - 0xFF220878: "Deep Blue", + 0xFF20B2AA: "Light Sea Green", 0xFF228B22: "Forest Green", - 0xFF233418: "Mallard", - 0xFF240A40: "Violet", - 0xFF240C02: "Kilamanjaro", - 0xFF242A1D: "Log Cabin", - 0xFF242E16: "Black Olive", - 0xFF24500F: "Green House", - 0xFF251607: "Graphite", - 0xFF251706: "Cannon Black", - 0xFF251F4F: "Port Gore", - 0xFF25272C: "Shark", - 0xFF25311C: "Green Kelp", - 0xFF2596D1: "Curious Blue", - 0xFF260368: "Paua", - 0xFF26056A: "Paris M", - 0xFF261105: "Wood Bark", - 0xFF261414: "Gondola", - 0xFF262335: "Steel Gray", - 0xFF26283B: "Ebony Clay", - 0xFF273A81: "Bay of Many", - 0xFF27504B: "Plantation", - 0xFF278A5B: "Eucalyptus", - 0xFF281E15: "Oil", - 0xFF283A77: "Astronaut", - 0xFF286ACD: "Mariner", - 0xFF290C5E: "Violent Violet", - 0xFF292130: "Bastille", - 0xFF292319: "Zeus", - 0xFF292937: "Charade", - 0xFF297B9A: "Jelly Bean", - 0xFF29AB87: "Jungle Green", - 0xFF2A0359: "Cherry Pie", - 0xFF2A140E: "Coffee Bean", - 0xFF2A2630: "Baltic Sea", - 0xFF2A380B: "Turtle Green", - 0xFF2A52BE: "Cerulean Blue", - 0xFF2B0202: "Sepia Black", - 0xFF2B194F: "Valhalla", - 0xFF2B3228: "Heavy Metal", - 0xFF2C0E8C: "Blue Gem", - 0xFF2C1632: "Revolver", - 0xFF2C2133: "Bleached Cedar", - 0xFF2C8C84: "Lochinvar", - 0xFF2D2510: "Mikado", - 0xFF2D383A: "Outer Space", - 0xFF2D569B: "St Tropaz", - 0xFF2E0329: "Jacaranda", - 0xFF2E1905: "Jacko Bean", - 0xFF2E3222: "Rangitoto", - 0xFF2E3F62: "Rhino", 0xFF2E8B57: "Sea Green", - 0xFF2EBFD4: "Scooter", - 0xFF2F270E: "Onion", - 0xFF2F3CB3: "Governor Bay", - 0xFF2F519E: "Sapphire", - 0xFF2F5A57: "Spectra", - 0xFF2F6168: "Casal", - 0xFF300529: "Melanzane", - 0xFF301F1E: "Cocoa Brown", - 0xFF302A0F: "Woodrush", - 0xFF304B6A: "San Juan", - 0xFF30D5C8: "Turquoise", - 0xFF311C17: "Eclipse", - 0xFF314459: "Pickled Bluewood", - 0xFF315BA1: "Azure", - 0xFF31728D: "Calypso", - 0xFF317D82: "Paradiso", - 0xFF32127A: "Persian Indigo", - 0xFF32293A: "Blackcurrant", - 0xFF323232: "Mine Shaft", - 0xFF325D52: "Stromboli", - 0xFF327C14: "Bilbao", - 0xFF327DA0: "Astral", - 0xFF33036B: "Christalle", - 0xFF33292F: "Thunder", - 0xFF33CC99: "Shamrock", - 0xFF341515: "Tamarind", - 0xFF350036: "Mardi Gras", - 0xFF350E42: "Valentino", - 0xFF350E57: "Jagger", - 0xFF353542: "Tuna", - 0xFF354E8C: "Chambray", - 0xFF363050: "Martinique", - 0xFF363534: "Tuatara", - 0xFF363C0D: "Waiouru", - 0xFF36747D: "Ming", - 0xFF368716: "La Palma", - 0xFF370202: "Chocolate", - 0xFF371D09: "Clinker", - 0xFF37290E: "Brown Tumbleweed", - 0xFF373021: "Birch", - 0xFF377475: "Oracle", - 0xFF380474: "Blue Diamond", - 0xFF381A51: "Grape", - 0xFF383533: "Dune", - 0xFF384555: "Oxford Blue", - 0xFF384910: "Clover", - 0xFF394851: "Limed Spruce", - 0xFF396413: "Dell", - 0xFF3A0020: "Toledo", - 0xFF3A2010: "Sambuca", - 0xFF3A2A6A: "Jacarta", - 0xFF3A686C: "William", - 0xFF3A6A47: "Killarney", - 0xFF3AB09E: "Keppel", - 0xFF3B000B: "Temptress", - 0xFF3B0910: "Aubergine", - 0xFF3B1F1F: "Jon", - 0xFF3B2820: "Treehouse", - 0xFF3B7A57: "Amazon", - 0xFF3B91B4: "Boston Blue", - 0xFF3C0878: "Windsor", - 0xFF3C1206: "Rebel", - 0xFF3C1F76: "Meteorite", - 0xFF3C2005: "Dark Ebony", - 0xFF3C3910: "Camouflage", - 0xFF3C4151: "Bright Gray", - 0xFF3C4443: "Cape Cod", - 0xFF3C493A: "Lunar Green", - 0xFF3D0C02: "Bean ", - 0xFF3D2B1F: "Bistre", - 0xFF3D7D52: "Goblin", - 0xFF3E0480: "Kingfisher Daisy", - 0xFF3E1C14: "Cedar", - 0xFF3E2B23: "English Walnut", - 0xFF3E2C1C: "Black Marlin", - 0xFF3E3A44: "Ship Gray", - 0xFF3EABBF: "Pelorous", - 0xFF3F2109: "Bronze", - 0xFF3F2500: "Cola", - 0xFF3F3002: "Madras", - 0xFF3F307F: "Minsk", - 0xFF3F4C3A: "Cabbage Pont", - 0xFF3F583B: "Tom Thumb", - 0xFF3F5D53: "Mineral Green", - 0xFF3FC1AA: "Puerto Rico", - 0xFF3FFF00: "Harlequin", - 0xFF401801: "Brown Pod", - 0xFF40291D: "Cork", - 0xFF403B38: "Masala", - 0xFF403D19: "Thatch Green", - 0xFF405169: "Fiord", - 0xFF40826D: "Viridian", - 0xFF40A860: "Chateau Green", - 0xFF410056: "Ripe Plum", - 0xFF411F10: "Paco", - 0xFF412010: "Deep Oak", - 0xFF413C37: "Merlin", - 0xFF414257: "Gun Powder", - 0xFF414C7D: "East Bay", + 0xFF2F4F4F: "Dark Slate Gray", + 0xFF32CD32: "Lime Green", + 0xFF3CB371: "Medium Sea Green", + 0xFF40E0D0: "Turquoise", 0xFF4169E1: "Royal Blue", - 0xFF41AA78: "Ocean Green", - 0xFF420303: "Burnt Maroon", - 0xFF423921: "Lisbon Brown", - 0xFF427977: "Faded Jade", - 0xFF431560: "Scarlet Gum", - 0xFF433120: "Iroko", - 0xFF433E37: "Armadillo", - 0xFF434C59: "River Bed", - 0xFF436A0D: "Green Leaf", - 0xFF44012D: "Barossa", - 0xFF441D00: "Morocco Brown", - 0xFF444954: "Mako", - 0xFF454936: "Kelp", - 0xFF456CAC: "San Marino", - 0xFF45B1E8: "Picton Blue", - 0xFF460B41: "Loulou", - 0xFF462425: "Crater Brown", - 0xFF465945: "Gray Asparagus", 0xFF4682B4: "Steel Blue", - 0xFF480404: "Rustic Red", - 0xFF480607: "Bulgarian Rose", - 0xFF480656: "Clairvoyant", - 0xFF481C1C: "Cocoa Bean", - 0xFF483131: "Woody Brown", - 0xFF483C32: "Taupe", - 0xFF49170C: "Van Cleef", - 0xFF492615: "Brown Derby", - 0xFF49371B: "Metallic Bronze", - 0xFF495400: "Verdun Green", - 0xFF496679: "Blue Bayoux", - 0xFF497183: "Bismark", - 0xFF4A2A04: "Bracken", - 0xFF4A3004: "Deep Bronze", - 0xFF4A3C30: "Mondo", - 0xFF4A4244: "Tundora", - 0xFF4A444B: "Gravel", - 0xFF4A4E5A: "Trout", - 0xFF4B0082: "Pigment Indigo", - 0xFF4B5D52: "Nandor", - 0xFF4C3024: "Saddle", - 0xFF4C4F56: "Abbey", - 0xFF4D0135: "Blackberry", - 0xFF4D0A18: "Cab Sav", - 0xFF4D1E01: "Indian Tan", - 0xFF4D282D: "Cowboy", - 0xFF4D282E: "Livid Brown", - 0xFF4D3833: "Rock", - 0xFF4D3D14: "Punga", - 0xFF4D400F: "Bronzetone", - 0xFF4D5328: "Woodland", - 0xFF4E0606: "Mahogany", - 0xFF4E2A5A: "Bossanova", - 0xFF4E3B41: "Matterhorn", - 0xFF4E420C: "Bronze Olive", - 0xFF4E4562: "Mulled Wine", - 0xFF4E6649: "Axolotl", - 0xFF4E7F9E: "Wedgewood", - 0xFF4EABD1: "Shakespeare", - 0xFF4F1C70: "Honey Flower", - 0xFF4F2398: "Daisy Bush", - 0xFF4F69C6: "Indigo", - 0xFF4F7942: "Fern Green", - 0xFF4F9D5D: "Fruit Salad", - 0xFF4FA83D: "Apple", - 0xFF504351: "Mortar", - 0xFF507096: "Kashmir Blue", - 0xFF507672: "Cutty Sark", - 0xFF50C878: "Emerald", - 0xFF514649: "Emperor", - 0xFF516E3D: "Chalet Green", - 0xFF517C66: "Como", - 0xFF51808F: "Smalt Blue", - 0xFF52001F: "Castro", - 0xFF520C17: "Maroon Oak", - 0xFF523C94: "Gigas", - 0xFF533455: "Voodoo", - 0xFF534491: "Victoria", - 0xFF53824B: "Hippie Green", - 0xFF541012: "Heath", - 0xFF544333: "Judge Gray", - 0xFF54534D: "Fuscous Gray", - 0xFF549019: "Vida Loca", - 0xFF55280C: "Cioccolato", - 0xFF555B10: "Saratoga", - 0xFF556D56: "Finlandia", - 0xFF5590D9: "Havelock Blue", - 0xFF56B4BE: "Fountain Blue", - 0xFF578363: "Spring Leaves", - 0xFF583401: "Saddle Brown", - 0xFF585562: "Scarpa Flow", - 0xFF587156: "Cactus", - 0xFF589AAF: "Hippie Blue", - 0xFF591D35: "Wine Berry", - 0xFF592804: "Brown Bramble", - 0xFF593737: "Congo Brown", - 0xFF594433: "Millbrook", - 0xFF5A6E9C: "Waikawa Gray", - 0xFF5A87A0: "Horizon", - 0xFF5B3013: "Jambalaya", - 0xFF5C0120: "Bordeaux", - 0xFF5C0536: "Mulberry Wood", - 0xFF5C2E01: "Carnaby Tan", - 0xFF5C5D75: "Comet", - 0xFF5D1E0F: "Redwood", - 0xFF5D4C51: "Don Juan", - 0xFF5D5C58: "Chicago", - 0xFF5D5E37: "Verdigris", - 0xFF5D7747: "Dingley", - 0xFF5DA19F: "Breaker Bay", - 0xFF5E483E: "Kabul", - 0xFF5E5D3B: "Hemlock", - 0xFF5F3D26: "Irish Coffee", - 0xFF5F5F6E: "Mid Gray", - 0xFF5F6672: "Shuttle Gray", - 0xFF5FA777: "Aqua Forest", - 0xFF5FB3AC: "Tradewind", - 0xFF604913: "Horses Neck", - 0xFF605B73: "Smoky", - 0xFF606E68: "Corduroy", - 0xFF6093D1: "Danube", - 0xFF612718: "Espresso", - 0xFF614051: "Eggplant", - 0xFF615D30: "Costa Del Sol", - 0xFF61845F: "Glade Green", - 0xFF622F30: "Buccaneer", - 0xFF623F2D: "Quincy", - 0xFF624E9A: "Butterfly Bush", - 0xFF625119: "West Coast", - 0xFF626649: "Finch", - 0xFF639A8F: "Patina", - 0xFF63B76C: "Fern", - 0xFF6456B7: "Blue Violet", - 0xFF646077: "Dolphin", - 0xFF646463: "Storm Dust", - 0xFF646A54: "Siam", - 0xFF646E75: "Nevada", + 0xFF483D8B: "Dark Slate Blue", + 0xFF48D1CC: "Medium Turquoise", + 0xFF4B0082: "Indigo", + 0xFF556B2F: "Dark Olive Green", + 0xFF5F9EA0: "Cadet Blue", 0xFF6495ED: "Cornflower Blue", - 0xFF64CCDB: "Viking", - 0xFF65000B: "Rosewood", - 0xFF651A14: "Cherrywood", - 0xFF652DC1: "Purple Heart", - 0xFF657220: "Fern Frond", - 0xFF65745D: "Willow Grove", - 0xFF65869F: "Hoki", - 0xFF660045: "Pompadour", - 0xFF660099: "Purple", - 0xFF66023C: "Tyrian Purple", - 0xFF661010: "Dark Tan", - 0xFF66B58F: "Silver Tree", - 0xFF66FF00: "Bright Green", - 0xFF66FF66: "Screamin' Green", - 0xFF67032D: "Black Rose", - 0xFF675FA6: "Scampi", - 0xFF676662: "Ironside Gray", - 0xFF678975: "Viridian Green", - 0xFF67A712: "Christi", - 0xFF683600: "Nutmeg Wood Finish", - 0xFF685558: "Zambezi", - 0xFF685E6E: "Salt Box", - 0xFF692545: "Tawny Port", - 0xFF692D54: "Finn", - 0xFF695F62: "Scorpion", - 0xFF697E9A: "Lynch", - 0xFF6A442E: "Spice", - 0xFF6A5D1B: "Himalaya", - 0xFF6A6051: "Soya Bean", - 0xFF6B2A14: "Hairy Heath", - 0xFF6B3FA0: "Royal Purple", - 0xFF6B4E31: "Shingle Fawn", - 0xFF6B5755: "Dorado", - 0xFF6B8BA2: "Bermuda Gray", + 0xFF66CDAA: "Medium Aquamarine", + 0xFF696969: "Dim Gray", + 0xFF6A5ACD: "Slate Blue", 0xFF6B8E23: "Olive Drab", - 0xFF6C3082: "Eminence", - 0xFF6CDAE7: "Turquoise Blue", - 0xFF6D0101: "Lonestar", - 0xFF6D5E54: "Pine Cone", - 0xFF6D6C6C: "Dove Gray", - 0xFF6D9292: "Juniper", - 0xFF6D92A1: "Gothic", - 0xFF6E0902: "Red Oxide", - 0xFF6E1D14: "Moccaccino", - 0xFF6E4826: "Pickled Bean", - 0xFF6E4B26: "Dallas", - 0xFF6E6D57: "Kokoda", - 0xFF6E7783: "Pale Sky", - 0xFF6F440C: "Cafe Royale", - 0xFF6F6A61: "Flint", - 0xFF6F8E63: "Highland", - 0xFF6F9D02: "Limeade", - 0xFF6FD0C5: "Downy", - 0xFF701C1C: "Persian Plum", - 0xFF704214: "Sepia", - 0xFF704A07: "Antique Bronze", - 0xFF704F50: "Ferra", - 0xFF706555: "Coffee", 0xFF708090: "Slate Gray", - 0xFF711A00: "Cedar Wood Finish", - 0xFF71291D: "Metallic Copper", - 0xFF714693: "Affair", - 0xFF714AB2: "Studio", - 0xFF715D47: "Tobacco Brown", - 0xFF716338: "Yellow Metal", - 0xFF716B56: "Peat", - 0xFF716E10: "Olivetone", - 0xFF717486: "Storm Gray", - 0xFF718080: "Sirocco", - 0xFF71D9E2: "Aquamarine Blue", - 0xFF72010F: "Venetian Red", - 0xFF724A2F: "Old Copper", - 0xFF726D4E: "Go Ben", - 0xFF727B89: "Raven", - 0xFF731E8F: "Seance", - 0xFF734A12: "Raw Umber", - 0xFF736C9F: "Kimberly", - 0xFF736D58: "Crocodile", - 0xFF737829: "Crete", - 0xFF738678: "Xanadu", - 0xFF74640D: "Spicy Mustard", - 0xFF747D63: "Limed Ash", - 0xFF747D83: "Rolling Stone", - 0xFF748881: "Blue Smoke", - 0xFF749378: "Laurel", - 0xFF74C365: "Mantis", - 0xFF755A57: "Russett", - 0xFF7563A8: "Deluge", - 0xFF76395D: "Cosmic", - 0xFF7666C6: "Blue Marguerite", - 0xFF76BD17: "Lima", - 0xFF76D7EA: "Sky Blue", - 0xFF770F05: "Dark Burgundy", - 0xFF771F1F: "Crown of Thorns", - 0xFF773F1A: "Walnut", - 0xFF776F61: "Pablo", - 0xFF778120: "Pacifika", - 0xFF779E86: "Oxley", - 0xFF77DD77: "Pastel Green", - 0xFF780109: "Japanese Maple", - 0xFF782D19: "Mocha", - 0xFF782F16: "Peanut", - 0xFF78866B: "Camouflage Green", - 0xFF788A25: "Wasabi", - 0xFF788BBA: "Ship Cove", - 0xFF78A39C: "Sea Nymph", - 0xFF795D4C: "Roman Coffee", - 0xFF796878: "Old Lavender", - 0xFF796989: "Rum", - 0xFF796A78: "Fedora", - 0xFF796D62: "Sandstone", - 0xFF79DEEC: "Spray", - 0xFF7A013A: "Siren", - 0xFF7A58C1: "Fuchsia Blue", - 0xFF7A7A7A: "Boulder", - 0xFF7A89B8: "Wild Blue Yonder", - 0xFF7AC488: "De York", - 0xFF7B3801: "Red Beech", - 0xFF7B3F00: "Cinnamon", - 0xFF7B6608: "Yukon Gold", - 0xFF7B7874: "Tapa", - 0xFF7B7C94: "Waterloo ", - 0xFF7B8265: "Flax Smoke", - 0xFF7B9F80: "Amulet", - 0xFF7BA05B: "Asparagus", - 0xFF7C1C05: "Kenyan Copper", - 0xFF7C7631: "Pesto", - 0xFF7C778A: "Topaz", - 0xFF7C7B7A: "Concord", - 0xFF7C7B82: "Jumbo", - 0xFF7C881A: "Trendy Green", - 0xFF7CA1A6: "Gumbo", - 0xFF7CB0A1: "Acapulco", - 0xFF7CB7BB: "Neptune", - 0xFF7D2C14: "Pueblo", - 0xFF7DA98D: "Bay Leaf", - 0xFF7DC8F7: "Malibu", - 0xFF7DD8C6: "Bermuda", - 0xFF7E3A15: "Copper Canyon", - 0xFF7F1734: "Claret", - 0xFF7F3A02: "Peru Tan", - 0xFF7F626D: "Falcon", - 0xFF7F7589: "Mobster", - 0xFF7F76D3: "Moody Blue", + 0xFF778899: "Light Slate Gray", + 0xFF7B68EE: "Medium Slate Blue", + 0xFF7CFC00: "Lawn Green", 0xFF7FFF00: "Chartreuse", 0xFF7FFFD4: "Aquamarine", 0xFF800000: "Maroon", - 0xFF800B47: "Rose Bud Cherry", - 0xFF801818: "Falu Red", - 0xFF80341F: "Red Robin", - 0xFF803790: "Vivid Violet", - 0xFF80461B: "Russet", - 0xFF807E79: "Friar Gray", + 0xFF800080: "Purple", 0xFF808000: "Olive", 0xFF808080: "Gray", - 0xFF80B3AE: "Gulf Stream", - 0xFF80B3C4: "Glacier", - 0xFF80CCEA: "Seagull", - 0xFF81422C: "Nutmeg", - 0xFF816E71: "Spicy Pink", - 0xFF817377: "Empress", - 0xFF819885: "Spanish Green", - 0xFF826F65: "Sand Dune", - 0xFF828685: "Gunsmoke", - 0xFF828F72: "Battleship Gray", - 0xFF831923: "Merlot", - 0xFF837050: "Shadow", - 0xFF83AA5D: "Chelsea Cucumber", - 0xFF83D0C6: "Monte Carlo", - 0xFF843179: "Plum", - 0xFF84A0A0: "Granny Smith", - 0xFF8581D9: "Chetwode Blue", - 0xFF858470: "Bandicoot", - 0xFF859FAF: "Bali Hai", - 0xFF85C4CC: "Half Baked", - 0xFF860111: "Red Devil", - 0xFF863C3C: "Lotus", - 0xFF86483C: "Ironstone", - 0xFF864D1E: "Bull Shot", - 0xFF86560A: "Rusty Nail", - 0xFF868974: "Bitter", - 0xFF86949F: "Regent Gray", - 0xFF871550: "Disco", - 0xFF87756E: "Americano", - 0xFF877C7B: "Hurricane", - 0xFF878D91: "Oslo Gray", - 0xFF87AB39: "Sushi", - 0xFF885342: "Spicy Mix", - 0xFF886221: "Kumera", - 0xFF888387: "Suva Gray", - 0xFF888D65: "Avocado", - 0xFF893456: "Camelot", - 0xFF893843: "Solid Pink", - 0xFF894367: "Cannon Pink", - 0xFF897D6D: "Makara", - 0xFF8A3324: "Burnt Umber", - 0xFF8A73D6: "True V", - 0xFF8A8360: "Clay Creek", - 0xFF8A8389: "Monsoon", - 0xFF8A8F8A: "Stack", - 0xFF8AB9F1: "Jordy Blue", - 0xFF8B00FF: "Electric Violet", - 0xFF8B0723: "Monarch", - 0xFF8B6B0B: "Corn Harvest", - 0xFF8B8470: "Olive Haze", - 0xFF8B847E: "Schooner", - 0xFF8B8680: "Natural Gray", - 0xFF8B9C90: "Mantle", - 0xFF8B9FEE: "Portage", - 0xFF8BA690: "Envy", - 0xFF8BA9A5: "Cascade", - 0xFF8BE6D8: "Riptide", - 0xFF8C055E: "Cardinal Pink", - 0xFF8C472F: "Mule Fawn", - 0xFF8C5738: "Potters Clay", - 0xFF8C6495: "Trendy Pink", - 0xFF8D0226: "Paprika", - 0xFF8D3D38: "Sanguine Brown", - 0xFF8D3F3F: "Tosca", - 0xFF8D7662: "Cement", - 0xFF8D8974: "Granite Green", - 0xFF8D90A1: "Manatee", - 0xFF8DA8CC: "Polo Blue", - 0xFF8E0000: "Red Berry", - 0xFF8E4D1E: "Rope", - 0xFF8E6F70: "Opium", - 0xFF8E775E: "Domino", - 0xFF8E8190: "Mamba", - 0xFF8EABC1: "Nepal", - 0xFF8F021C: "Pohutukawa", - 0xFF8F3E33: "El Salva", - 0xFF8F4B0E: "Korma", - 0xFF8F8176: "Squirrel", - 0xFF8FD6B4: "Vista Blue", - 0xFF900020: "Burgundy", - 0xFF901E1E: "Old Brick", - 0xFF907874: "Hemp", - 0xFF907B71: "Almond Frost", - 0xFF908D39: "Sycamore", - 0xFF92000A: "Sangria", - 0xFF924321: "Cumin", - 0xFF926F5B: "Beaver", - 0xFF928573: "Stonewall", - 0xFF928590: "Venus", + 0xFF87CEEB: "Sky Blue", + 0xFF87CEFA: "Light Sky Blue", + 0xFF8A2BE2: "Blue Violet", + 0xFF8B0000: "Dark Red", + 0xFF8B008B: "Dark Magenta", + 0xFF8B4513: "Saddle Brown", + 0xFF8FBC8F: "Dark Sea Green", + 0xFF90EE90: "Light Green", 0xFF9370DB: "Medium Purple", - 0xFF93CCEA: "Cornflower", - 0xFF93DFB8: "Algae Green", - 0xFF944747: "Copper Rust", - 0xFF948771: "Arrowtown", - 0xFF950015: "Scarlett", - 0xFF956387: "Strikemaster", - 0xFF959396: "Mountain Mist", - 0xFF960018: "Carmine", - 0xFF964B00: "Brown", - 0xFF967059: "Leather", - 0xFF9678B6: "Purple Mountain's Majesty", - 0xFF967BB6: "Lavender Purple", - 0xFF96A8A1: "Pewter", - 0xFF96BBAB: "Summer Green", - 0xFF97605D: "Au Chico", - 0xFF9771B5: "Wisteria", - 0xFF97CD2D: "Atlantis", - 0xFF983D61: "Vin Rouge", - 0xFF9874D3: "Lilac Bush", - 0xFF98777B: "Bazaar", - 0xFF98811B: "Hacienda", - 0xFF988D77: "Pale Oyster", - 0xFF98FF98: "Mint Green", - 0xFF990066: "Fresh Eggplant", - 0xFF991199: "Violet Eggplant", - 0xFF991613: "Tamarillo", - 0xFF991B07: "Totem Pole", - 0xFF996666: "Copper Rose", - 0xFF9966CC: "Amethyst", - 0xFF997A8D: "Mountbatten Pink", - 0xFF9999CC: "Blue Bell", - 0xFF9A3820: "Prairie Sand", - 0xFF9A6E61: "Toast", - 0xFF9A9577: "Gurkha", - 0xFF9AB973: "Olivine", - 0xFF9AC2B8: "Shadow Green", - 0xFF9B4703: "Oregon", - 0xFF9B9E8F: "Lemon Grass", - 0xFF9C3336: "Stiletto", - 0xFF9D5616: "Hawaiian Tan", - 0xFF9DACB7: "Gull Gray", - 0xFF9DC209: "Pistachio", - 0xFF9DE093: "Granny Smith Apple", - 0xFF9DE5FF: "Anakiwa", - 0xFF9E5302: "Chelsea Gem", - 0xFF9E5B40: "Sepia Skin", - 0xFF9EA587: "Sage", - 0xFF9EA91F: "Citron", - 0xFF9EB1CD: "Rock Blue", - 0xFF9EDEE0: "Morning Glory", - 0xFF9F381D: "Cognac", - 0xFF9F821C: "Reef Gold", - 0xFF9F9F9C: "Star Dust", - 0xFF9FA0B1: "Santas Gray", - 0xFF9FD7D3: "Sinbad", - 0xFF9FDD8C: "Feijoa", - 0xFFA02712: "Tabasco", - 0xFFA1750D: "Buttered Rum", - 0xFFA1ADB5: "Hit Gray", - 0xFFA1C50A: "Citrus", - 0xFFA1DAD7: "Aqua Island", - 0xFFA1E9DE: "Water Leaf", - 0xFFA2006D: "Flirt", - 0xFFA23B6C: "Rouge", - 0xFFA26645: "Cape Palliser", - 0xFFA2AAB3: "Gray Chateau", - 0xFFA2AEAB: "Edward", - 0xFFA3807B: "Pharlap", - 0xFFA397B4: "Amethyst Smoke", - 0xFFA3E3ED: "Blizzard Blue", - 0xFFA4A49D: "Delta", - 0xFFA4A6D3: "Wistful", - 0xFFA4AF6E: "Green Smoke", - 0xFFA50B5E: "Jazzberry Jam", - 0xFFA59B91: "Zorba", - 0xFFA5CB0C: "Bahia", - 0xFFA62F20: "Roof Terracotta", - 0xFFA65529: "Paarl", - 0xFFA68B5B: "Barley Corn", - 0xFFA69279: "Donkey Brown", - 0xFFA6A29A: "Dawn", - 0xFFA72525: "Mexican Red", - 0xFFA7882C: "Luxor Gold", - 0xFFA85307: "Rich Gold", - 0xFFA86515: "Reno Sand", - 0xFFA86B6B: "Coral Tree", - 0xFFA8989B: "Dusty Gray", - 0xFFA899E6: "Dull Lavender", - 0xFFA8A589: "Tallow", - 0xFFA8AE9C: "Bud", - 0xFFA8AF8E: "Locust", - 0xFFA8BD9F: "Norway", - 0xFFA8E3BD: "Chinook", - 0xFFA9A491: "Gray Olive", - 0xFFA9ACB6: "Aluminium", - 0xFFA9B2C3: "Cadet Blue", - 0xFFA9B497: "Schist", - 0xFFA9BDBF: "Tower Gray", - 0xFFA9BEF2: "Perano", - 0xFFA9C6C2: "Opal", - 0xFFAA375A: "Night Shadz", - 0xFFAA4203: "Fire", - 0xFFAA8B5B: "Muesli", - 0xFFAA8D6F: "Sandal", - 0xFFAAA5A9: "Shady Lady", - 0xFFAAA9CD: "Logan", - 0xFFAAABB7: "Spun Pearl", - 0xFFAAD6E6: "Regent St Blue", - 0xFFAAF0D1: "Magic Mint", - 0xFFAB0563: "Lipstick", - 0xFFAB3472: "Royal Heath", - 0xFFAB917A: "Sandrift", - 0xFFABA0D9: "Cold Purple", - 0xFFABA196: "Bronco", - 0xFFAC8A56: "Limed Oak", - 0xFFAC91CE: "East Side", - 0xFFAC9E22: "Lemon Ginger", - 0xFFACA494: "Napa", - 0xFFACA586: "Hillary", - 0xFFACA59F: "Cloudy", - 0xFFACACAC: "Silver Chalice", - 0xFFACB78E: "Swamp Green", - 0xFFACCBB1: "Spring Rain", - 0xFFACDD4D: "Conifer", - 0xFFACE1AF: "Celadon", - 0xFFAD781B: "Mandalay", - 0xFFADBED1: "Casper", - 0xFFADDFAD: "Moss Green", - 0xFFADE6C4: "Padua", + 0xFF9400D3: "Dark Violet", + 0xFF98FB98: "Pale Green", + 0xFF9932CC: "Dark Orchid", + 0xFF9ACD32: "Yellow Green", + 0xFFA0522D: "Sienna", + 0xFFA52A2A: "Brown", + 0xFFA9A9A9: "Dark Gray", + 0xFFADD8E6: "Light Blue", 0xFFADFF2F: "Green Yellow", - 0xFFAE4560: "Hippie Pink", - 0xFFAE6020: "Desert", - 0xFFAE809E: "Bouquet", - 0xFFAF4035: "Medium Carmine", - 0xFFAF4D43: "Apple Blossom", - 0xFFAF593E: "Brown Rust", - 0xFFAF8751: "Driftwood", - 0xFFAF8F2C: "Alpine", - 0xFFAF9F1C: "Lucky", - 0xFFAFA09E: "Martini", - 0xFFAFB1B8: "Bombay", - 0xFFAFBDD9: "Pigeon Post", - 0xFFB04C6A: "Cadillac", - 0xFFB05D54: "Matrix", - 0xFFB05E81: "Tapestry", - 0xFFB06608: "Mai Tai", - 0xFFB09A95: "Del Rio", + 0xFFAFEEEE: "Pale Turquoise", + 0xFFB0C4DE: "Light Steel Blue", 0xFFB0E0E6: "Powder Blue", - 0xFFB0E313: "Inch Worm", - 0xFFB10000: "Bright Red", - 0xFFB14A0B: "Vesuvius", - 0xFFB1610B: "Pumpkin Skin", - 0xFFB16D52: "Santa Fe", - 0xFFB19461: "Teak", - 0xFFB1E2C1: "Fringy Flower", - 0xFFB1F4E7: "Ice Cold", - 0xFFB20931: "Shiraz", - 0xFFB2A1EA: "Biloba Flower", - 0xFFB32D29: "Tall Poppy", - 0xFFB35213: "Fiery Orange", - 0xFFB38007: "Hot Toddy", - 0xFFB3AF95: "Taupe Gray", - 0xFFB3C110: "La Rioja", - 0xFFB43332: "Well Read", - 0xFFB44668: "Blush", - 0xFFB4CFD3: "Jungle Mist", - 0xFFB57281: "Turkish Rose", - 0xFFB57EDC: "Lavender", - 0xFFB5A27F: "Mongoose", - 0xFFB5B35C: "Olive Green", - 0xFFB5D2CE: "Jet Stream", - 0xFFB5ECDF: "Cruise", - 0xFFB6316C: "Hibiscus", - 0xFFB69D98: "Thatch", - 0xFFB6B095: "Heathered Gray", - 0xFFB6BAA4: "Eagle", - 0xFFB6D1EA: "Spindle", - 0xFFB6D3BF: "Gum Leaf", - 0xFFB7410E: "Rust", - 0xFFB78E5C: "Muddy Waters", - 0xFFB7A214: "Sahara", - 0xFFB7A458: "Husk", - 0xFFB7B1B1: "Nobel", - 0xFFB7C3D0: "Heather", - 0xFFB7F0BE: "Madang", - 0xFFB81104: "Milano Red", - 0xFFB87333: "Copper", - 0xFFB8B56A: "Gimblet", - 0xFFB8C1B1: "Green Spring", - 0xFFB8C25D: "Celery", - 0xFFB8E0F9: "Sail", - 0xFFB94E48: "Chestnut", - 0xFFB95140: "Crail", - 0xFFB98D28: "Marigold", - 0xFFB9C46A: "Wild Willow", - 0xFFB9C8AC: "Rainee", - 0xFFBA0101: "Guardsman Red", - 0xFFBA450C: "Rock Spray", - 0xFFBA6F1E: "Bourbon", - 0xFFBA7F03: "Pirate Gold", - 0xFFBAB1A2: "Nomad", - 0xFFBAC7C9: "Submarine", - 0xFFBAEEF9: "Charlotte", - 0xFFBB3385: "Medium Red Violet", - 0xFFBB8983: "Brandy Rose", - 0xFFBBD009: "Rio Grande", - 0xFFBBD7C1: "Surf", - 0xFFBCC9C2: "Powder Ash", - 0xFFBD5E2E: "Tuscany", - 0xFFBD978E: "Quicksand", - 0xFFBDB1A8: "Silk", - 0xFFBDB2A1: "Malta", - 0xFFBDB3C7: "Chatelle", - 0xFFBDBBD7: "Lavender Gray", - 0xFFBDBDC6: "French Gray", - 0xFFBDC8B3: "Clay Ash", - 0xFFBDC9CE: "Loblolly", - 0xFFBDEDFD: "French Pass", - 0xFFBEA6C3: "London Hue", - 0xFFBEB5B7: "Pink Swan", - 0xFFBEDE0D: "Fuego", - 0xFFBF5500: "Rose of Sharon", - 0xFFBFB8B0: "Tide", - 0xFFBFBED8: "Blue Haze", - 0xFFBFC1C2: "Silver Sand", - 0xFFBFC921: "Key Lime Pie", - 0xFFBFDBE2: "Ziggurat", - 0xFFBFFF00: "Lime", - 0xFFC02B18: "Thunderbird", - 0xFFC04737: "Mojo", - 0xFFC08081: "Old Rose", + 0xFFB22222: "Firebrick", + 0xFFB8860B: "Dark Goldenrod", + 0xFFBA55D3: "Medium Orchid", + 0xFFBC8F8F: "Rosy Brown", + 0xFFBDB76B: "Dark Khaki", 0xFFC0C0C0: "Silver", - 0xFFC0D3B9: "Pale Leaf", - 0xFFC0D8B6: "Pixie Green", - 0xFFC1440E: "Tia Maria", - 0xFFC154C1: "Fuchsia Pink", - 0xFFC1A004: "Buddha Gold", - 0xFFC1B7A4: "Bison Hide", - 0xFFC1BAB0: "Tea", - 0xFFC1BECD: "Gray Suit", - 0xFFC1D7B0: "Sprout", - 0xFFC1F07C: "Sulu", - 0xFFC26B03: "Indochine", - 0xFFC2955D: "Twine", - 0xFFC2BDB6: "Cotton Seed", - 0xFFC2CAC4: "Pumice", - 0xFFC2E8E5: "Jagged Ice", - 0xFFC32148: "Maroon Flush", - 0xFFC3B091: "Indian Khaki", - 0xFFC3BFC1: "Pale Slate", - 0xFFC3C3BD: "Gray Nickel", - 0xFFC3CDE6: "Periwinkle Gray", - 0xFFC3D1D1: "Tiara", - 0xFFC3DDF9: "Tropical Blue", - 0xFFC41E3A: "Cardinal", - 0xFFC45655: "Fuzzy Wuzzy Brown", - 0xFFC45719: "Orange Roughy", - 0xFFC4C4BC: "Mist Gray", - 0xFFC4D0B0: "Coriander", - 0xFFC4F4EB: "Mint Tulip", - 0xFFC54B8C: "Mulberry", - 0xFFC59922: "Nugget", - 0xFFC5994B: "Tussock", - 0xFFC5DBCA: "Sea Mist", - 0xFFC5E17A: "Yellow Green", - 0xFFC62D42: "Brick Red", - 0xFFC6726B: "Contessa", - 0xFFC69191: "Oriental Pink", - 0xFFC6A84B: "Roti", - 0xFFC6C3B5: "Ash", - 0xFFC6C8BD: "Kangaroo", - 0xFFC6E610: "Las Palmas", - 0xFFC7031E: "Monza", - 0xFFC71585: "Red Violet", - 0xFFC7BCA2: "Coral Reef", - 0xFFC7C1FF: "Melrose", - 0xFFC7C4BF: "Cloud", - 0xFFC7C9D5: "Ghost", - 0xFFC7CD90: "Pine Glade", - 0xFFC7DDE5: "Botticelli", - 0xFFC88A65: "Antique Brass", - 0xFFC8A2C8: "Lilac", - 0xFFC8A528: "Hokey Pokey", - 0xFFC8AABF: "Lily", - 0xFFC8B568: "Laser", - 0xFFC8E3D7: "Edgewater", - 0xFFC96323: "Piper", - 0xFFC99415: "Pizza", - 0xFFC9A0DC: "Light Wisteria", - 0xFFC9B29B: "Rodeo Dust", - 0xFFC9B35B: "Sundance", - 0xFFC9B93B: "Earls Green", - 0xFFC9C0BB: "Silver Rust", - 0xFFC9D9D2: "Conch", - 0xFFC9FFA2: "Reef", - 0xFFC9FFE5: "Aero Blue", - 0xFFCA3435: "Flush Mahogany", - 0xFFCABB48: "Turmeric", - 0xFFCADCD4: "Paris White", - 0xFFCAE00D: "Bitter Lemon", - 0xFFCAE6DA: "Skeptic", - 0xFFCB8FA9: "Viola", - 0xFFCBCAB6: "Foggy Gray", - 0xFFCBD3B0: "Green Mist", - 0xFFCBDBD6: "Nebula", - 0xFFCC3333: "Persian Red", - 0xFFCC5500: "Burnt Orange", - 0xFFCC7722: "Ochre", - 0xFFCC8899: "Puce", - 0xFFCCCAA8: "Thistle Green", - 0xFFCCCCFF: "Periwinkle", - 0xFFCCFF00: "Electric Lime", - 0xFFCD5700: "Tenn", - 0xFFCD5C5C: "Chestnut Rose", - 0xFFCD8429: "Brandy Punch", - 0xFFCDF4FF: "Onahau", - 0xFFCEB98F: "Sorrell Brown", - 0xFFCEBABA: "Cold Turkey", - 0xFFCEC291: "Yuma", - 0xFFCEC7A7: "Chino", - 0xFFCFA39D: "Eunry", - 0xFFCFB53B: "Old Gold", - 0xFFCFDCCF: "Tasman", - 0xFFCFE5D2: "Surf Crest", - 0xFFCFF9F3: "Humming Bird", - 0xFFCFFAF4: "Scandal", - 0xFFD05F04: "Red Stage", - 0xFFD06DA1: "Hopbush", - 0xFFD07D12: "Meteor", - 0xFFD0BEF8: "Perfume", - 0xFFD0C0E5: "Prelude", - 0xFFD0F0C0: "Tea Green", - 0xFFD18F1B: "Geebung", - 0xFFD1BEA8: "Vanilla", - 0xFFD1C6B4: "Soft Amber", - 0xFFD1D2CA: "Celeste", - 0xFFD1D2DD: "Mischka", - 0xFFD1E231: "Pear", - 0xFFD2691E: "Hot Cinnamon", - 0xFFD27D46: "Raw Sienna", - 0xFFD29EAA: "Careys Pink", + 0xFFC71585: "Medium Violet Red", + 0xFFCD5C5C: "Indian Red", + 0xFFCD853F: "Peru", + 0xFFD2691E: "Chocolate", 0xFFD2B48C: "Tan", - 0xFFD2DA97: "Deco", - 0xFFD2F6DE: "Blue Romance", - 0xFFD2F8B0: "Gossip", - 0xFFD3CBBA: "Sisal", - 0xFFD3CDC5: "Swirl", - 0xFFD47494: "Charm", - 0xFFD4B6AF: "Clam Shell", - 0xFFD4BF8D: "Straw", - 0xFFD4C4A8: "Akaroa", - 0xFFD4CD16: "Bird Flower", - 0xFFD4D7D9: "Iron", - 0xFFD4DFE2: "Geyser", - 0xFFD4E2FC: "Hawkes Blue", - 0xFFD54600: "Grenadier", - 0xFFD591A4: "Can Can", - 0xFFD59A6F: "Whiskey", - 0xFFD5D195: "Winter Hazel", - 0xFFD5F6E3: "Granny Apple", - 0xFFD69188: "My Pink", - 0xFFD6C562: "Tacha", - 0xFFD6CEF6: "Moon Raker", - 0xFFD6D6D1: "Quill Gray", - 0xFFD6FFDB: "Snowy Mint", - 0xFFD7837F: "New York Pink", - 0xFFD7C498: "Pavlova", - 0xFFD7D0FF: "Fog", - 0xFFD84437: "Valencia", - 0xFFD87C63: "Japonica", + 0xFFD3D3D3: "Light Gray", 0xFFD8BFD8: "Thistle", - 0xFFD8C2D5: "Maverick", - 0xFFD8FCFA: "Foam", - 0xFFD94972: "Cabaret", - 0xFFD99376: "Burning Sand", - 0xFFD9B99B: "Cameo", - 0xFFD9D6CF: "Timberwolf", - 0xFFD9DCC1: "Tana", - 0xFFD9E4F5: "Link Water", - 0xFFD9F7FF: "Mabel", - 0xFFDA3287: "Cerise", - 0xFFDA5B38: "Flame Pea", - 0xFFDA6304: "Bamboo", - 0xFFDA6A41: "Red Damask", 0xFFDA70D6: "Orchid", - 0xFFDA8A67: "Copperfield", - 0xFFDAA520: "Golden Grass", - 0xFFDAECD6: "Zanah", - 0xFFDAF4F0: "Iceberg", - 0xFFDAFAFF: "Oyster Bay", - 0xFFDB5079: "Cranberry", - 0xFFDB9690: "Petite Orchid", - 0xFFDB995E: "Di Serria", - 0xFFDBDBDB: "Alto", - 0xFFDBFFF8: "Frosted Mint", + 0xFFDAA520: "Goldenrod", + 0xFFDB7093: "Pale Violet Red", 0xFFDC143C: "Crimson", - 0xFFDC4333: "Punch", - 0xFFDCB20C: "Galliano", - 0xFFDCB4BC: "Blossom", - 0xFFDCD747: "Wattle", - 0xFFDCD9D2: "Westar", - 0xFFDCDDCC: "Moon Mist", - 0xFFDCEDB4: "Caper", - 0xFFDCF0EA: "Swans Down", - 0xFFDDD6D5: "Swiss Coffee", - 0xFFDDF9F1: "White Ice", - 0xFFDE3163: "Cerise Red", - 0xFFDE6360: "Roman", - 0xFFDEA681: "Tumbleweed", - 0xFFDEBA13: "Gold Tips", - 0xFFDEC196: "Brandy", - 0xFFDECBC6: "Wafer", - 0xFFDED4A4: "Sapling", - 0xFFDED717: "Barberry", - 0xFFDEE5C0: "Beryl Green", - 0xFFDEF5FF: "Pattens Blue", - 0xFFDF73FF: "Heliotrope", - 0xFFDFBE6F: "Apache", - 0xFFDFCD6F: "Chenin", - 0xFFDFCFDB: "Lola", - 0xFFDFECDA: "Willow Brook", - 0xFFDFFF00: "Chartreuse Yellow", - 0xFFE0B0FF: "Mauve", - 0xFFE0B646: "Anzac", - 0xFFE0B974: "Harvest Gold", - 0xFFE0C095: "Calico", - 0xFFE0FFFF: "Baby Blue", - 0xFFE16865: "Sunglo", - 0xFFE1BC64: "Equator", - 0xFFE1C0C8: "Pink Flare", - 0xFFE1E6D6: "Periglacial Blue", - 0xFFE1EAD4: "Kidnapper", - 0xFFE1F6E8: "Tara", - 0xFFE25465: "Mandy", - 0xFFE2725B: "Terracotta", - 0xFFE28913: "Golden Bell", - 0xFFE292C0: "Shocking", - 0xFFE29418: "Dixie", - 0xFFE29CD2: "Light Orchid", - 0xFFE2D8ED: "Snuff", - 0xFFE2EBED: "Mystic", - 0xFFE2F3EC: "Apple Green", - 0xFFE30B5C: "Razzmatazz", - 0xFFE32636: "Alizarin Crimson", - 0xFFE34234: "Cinnabar", - 0xFFE3BEBE: "Cavern Pink", - 0xFFE3F5E1: "Peppermint", - 0xFFE3F988: "Mindaro", - 0xFFE47698: "Deep Blush", - 0xFFE49B0F: "Gamboge", - 0xFFE4C2D5: "Melanie", - 0xFFE4CFDE: "Twilight", - 0xFFE4D1C0: "Bone", - 0xFFE4D422: "Sunflower", - 0xFFE4D5B7: "Grain Brown", - 0xFFE4D69B: "Zombie", - 0xFFE4F6E7: "Frostee", - 0xFFE4FFD1: "Snow Flurry", - 0xFFE52B50: "Amaranth", - 0xFFE5841B: "Zest", - 0xFFE5CCC9: "Dust Storm", - 0xFFE5D7BD: "Stark White", - 0xFFE5D8AF: "Hampton", - 0xFFE5E0E1: "Bon Jour", - 0xFFE5E5E5: "Mercury", - 0xFFE5F9F6: "Polar", - 0xFFE64E03: "Trinidad", - 0xFFE6BE8A: "Gold Sand", - 0xFFE6BEA5: "Cashmere", - 0xFFE6D7B9: "Double Spanish White", - 0xFFE6E4D4: "Satin Linen", - 0xFFE6F2EA: "Harp", - 0xFFE6F8F3: "Off Green", - 0xFFE6FFE9: "Hint of Green", - 0xFFE6FFFF: "Tranquil", - 0xFFE77200: "Mango Tango", - 0xFFE7730A: "Christine", - 0xFFE79F8C: "Tonys Pink", - 0xFFE79FC4: "Kobi", - 0xFFE7BCB4: "Rose Fog", - 0xFFE7BF05: "Corn", - 0xFFE7CD8C: "Putty", - 0xFFE7ECE6: "Gray Nurse", - 0xFFE7F8FF: "Lily White", - 0xFFE7FEFF: "Bubbles", - 0xFFE89928: "Fire Bush", - 0xFFE8B9B3: "Shilo", - 0xFFE8E0D5: "Pearl Bush", - 0xFFE8EBE0: "Green White", - 0xFFE8F1D4: "Chrome White", - 0xFFE8F2EB: "Gin", - 0xFFE8F5F2: "Aqua Squeeze", - 0xFFE96E00: "Clementine", - 0xFFE97451: "Burnt Sienna", - 0xFFE97C07: "Tahiti Gold", - 0xFFE9CECD: "Oyster Pink", - 0xFFE9D75A: "Confetti", - 0xFFE9E3E3: "Ebb", - 0xFFE9F8ED: "Ottoman", - 0xFFE9FFFD: "Clear Day", - 0xFFEA88A8: "Carissma", - 0xFFEAAE69: "Porsche", - 0xFFEAB33B: "Tulip Tree", - 0xFFEAC674: "Rob Roy", - 0xFFEADAB8: "Raffia", - 0xFFEAE8D4: "White Rock", - 0xFFEAF6EE: "Panache", - 0xFFEAF6FF: "Solitude", - 0xFFEAF9F5: "Aqua Spring", - 0xFFEAFFFE: "Dew", - 0xFFEB9373: "Apricot", - 0xFFEBC2AF: "Zinnwaldite", - 0xFFECA927: "Fuel Yellow", - 0xFFECC54E: "Ronchi", - 0xFFECC7EE: "French Lilac", - 0xFFECCDB9: "Just Right", - 0xFFECE090: "Wild Rice", - 0xFFECEBBD: "Fall Green", - 0xFFECEBCE: "Aths Special", - 0xFFECF245: "Starship", - 0xFFED0A3F: "Red Ribbon", - 0xFFED7A1C: "Tango", - 0xFFED9121: "Carrot Orange", - 0xFFED989E: "Sea Pink", - 0xFFEDB381: "Tacao", - 0xFFEDC9AF: "Desert Sand", - 0xFFEDCDAB: "Pancho", - 0xFFEDDCB1: "Chamois", - 0xFFEDEA99: "Primrose", - 0xFFEDF5DD: "Frost", - 0xFFEDF5F5: "Aqua Haze", - 0xFFEDF6FF: "Zumthor", - 0xFFEDF9F1: "Narvik", - 0xFFEDFC84: "Honeysuckle", - 0xFFEE82EE: "Lavender Magenta", - 0xFFEEC1BE: "Beauty Bush", - 0xFFEED794: "Chalky", - 0xFFEED9C4: "Almond", - 0xFFEEDC82: "Flax", - 0xFFEEDEDA: "Bizarre", - 0xFFEEE3AD: "Double Colonial White", - 0xFFEEEEE8: "Cararra", - 0xFFEEEF78: "Manz", - 0xFFEEF0C8: "Tahuna Sands", - 0xFFEEF0F3: "Athens Gray", - 0xFFEEF3C3: "Tusk", - 0xFFEEF4DE: "Loafer", - 0xFFEEF6F7: "Catskill White", - 0xFFEEFDFF: "Twilight Blue", - 0xFFEEFF9A: "Jonquil", - 0xFFEEFFE2: "Rice Flower", - 0xFFEF863F: "Jaffa", - 0xFFEFEFEF: "Gallery", - 0xFFEFF2F3: "Porcelain", - 0xFFF091A9: "Mauvelous", - 0xFFF0D52D: "Golden Dream", - 0xFFF0DB7D: "Golden Sand", - 0xFFF0DC82: "Buff", - 0xFFF0E2EC: "Prim", + 0xFFDCDCDC: "Gainsboro", + 0xFFDDA0DD: "Plum", + 0xFFDEB887: "Burly Wood", + 0xFFE0FFFF: "Light Cyan", + 0xFFE6E6FA: "Lavender", + 0xFFE9967A: "Dark Salmon", + 0xFFEE82EE: "Violet", + 0xFFEEE8AA: "Pale Goldenrod", + 0xFFF08080: "Light Coral", 0xFFF0E68C: "Khaki", - 0xFFF0EEFD: "Selago", - 0xFFF0EEFF: "Titan White", 0xFFF0F8FF: "Alice Blue", - 0xFFF0FCEA: "Feta", - 0xFFF18200: "Gold Drop", - 0xFFF19BAB: "Wewak", - 0xFFF1E788: "Sahara Sand", - 0xFFF1E9D2: "Parchment", - 0xFFF1E9FF: "Blue Chalk", - 0xFFF1EEC1: "Mint Julep", - 0xFFF1F1F1: "Seashell", - 0xFFF1F7F2: "Saltpan", - 0xFFF1FFAD: "Tidal", - 0xFFF1FFC8: "Chiffon", - 0xFFF2552A: "Flamingo", - 0xFFF28500: "Tangerine", - 0xFFF2C3B2: "Mandys Pink", - 0xFFF2F2F2: "Concrete", - 0xFFF2FAFA: "Black Squeeze", - 0xFFF34723: "Pomegranate", - 0xFFF3AD16: "Buttercup", - 0xFFF3D69D: "New Orleans", - 0xFFF3D9DF: "Vanilla Ice", - 0xFFF3E7BB: "Sidecar", - 0xFFF3E9E5: "Dawn Pink", - 0xFFF3EDCF: "Wheatfield", - 0xFFF3FB62: "Canary", - 0xFFF3FBD4: "Orinoco", - 0xFFF3FFD8: "Carla", - 0xFFF400A1: "Hollywood Cerise", - 0xFFF4A460: "Sandy brown", - 0xFFF4C430: "Saffron", - 0xFFF4D81C: "Ripe Lemon", - 0xFFF4EBD3: "Janna", - 0xFFF4F2EE: "Pampas", - 0xFFF4F4F4: "Wild Sand", - 0xFFF4F8FF: "Zircon", - 0xFFF57584: "Froly", - 0xFFF5C85C: "Cream Can", - 0xFFF5C999: "Manhattan", - 0xFFF5D5A0: "Maize", + 0xFFF0FFF0: "Honeydew", + 0xFFF0FFFF: "Azure", + 0xFFF4A460: "Sandy Brown", 0xFFF5DEB3: "Wheat", - 0xFFF5E7A2: "Sandwisp", - 0xFFF5E7E2: "Pot Pourri", - 0xFFF5E9D3: "Albescent White", - 0xFFF5EDEF: "Soft Peach", - 0xFFF5F3E5: "Ecru White", 0xFFF5F5DC: "Beige", - 0xFFF5FB3D: "Golden Fizz", - 0xFFF5FFBE: "Australian Mint", - 0xFFF64A8A: "French Rose", - 0xFFF653A6: "Brilliant Rose", - 0xFFF6A4C9: "Illusion", - 0xFFF6F0E6: "Merino", - 0xFFF6F7F7: "Black Haze", - 0xFFF6FFDC: "Spring Sun", - 0xFFF7468A: "Violet Red", - 0xFFF77703: "Chilean Fire", - 0xFFF77FBE: "Persian Pink", - 0xFFF7B668: "Rajah", - 0xFFF7C8DA: "Azalea", - 0xFFF7DBE6: "We Peep", - 0xFFF7F2E1: "Quarter Spanish White", - 0xFFF7F5FA: "Whisper", - 0xFFF7FAF7: "Snow Drift", - 0xFFF8B853: "Casablanca", - 0xFFF8C3DF: "Chantilly", - 0xFFF8D9E9: "Cherub", - 0xFFF8DB9D: "Marzipan", - 0xFFF8DD5C: "Energy Yellow", - 0xFFF8E4BF: "Givry", - 0xFFF8F0E8: "White Linen", - 0xFFF8F4FF: "Magnolia", - 0xFFF8F6F1: "Spring Wood", - 0xFFF8F7DC: "Coconut Cream", - 0xFFF8F7FC: "White Lilac", - 0xFFF8F8F7: "Desert Storm", - 0xFFF8F99C: "Texas", - 0xFFF8FACD: "Corn Field", - 0xFFF8FDD3: "Mimosa", - 0xFFF95A61: "Carnation", - 0xFFF9BF58: "Saffron Mango", - 0xFFF9E0ED: "Carousel Pink", - 0xFFF9E4BC: "Dairy Cream", - 0xFFF9E663: "Portica", - 0xFFF9EAF3: "Amour", - 0xFFF9F8E4: "Rum Swizzle", - 0xFFF9FF8B: "Dolly", - 0xFFF9FFF6: "Sugar Cane", - 0xFFFA7814: "Ecstasy", - 0xFFFA9D5A: "Tan Hide", - 0xFFFAD3A2: "Corvette", - 0xFFFADFAD: "Peach Yellow", - 0xFFFAE600: "Turbo", - 0xFFFAEAB9: "Astra", - 0xFFFAECCC: "Champagne", + 0xFFF5F5F5: "White Smoke", + 0xFFF5FFFA: "Mint Cream", + 0xFFF8F8FF: "Ghost White", + 0xFFFA8072: "Salmon", + 0xFFFAEBD7: "Antique White", 0xFFFAF0E6: "Linen", - 0xFFFAF3F0: "Fantasy", - 0xFFFAF7D6: "Citrine White", - 0xFFFAFAFA: "Alabaster", - 0xFFFAFDE4: "Hint of Yellow", - 0xFFFAFFA4: "Milan", - 0xFFFB607F: "Brink Pink", - 0xFFFB8989: "Geraldine", - 0xFFFBA0E3: "Lavender Rose", - 0xFFFBA129: "Sea Buckthorn", - 0xFFFBAC13: "Sun", - 0xFFFBAED2: "Lavender Pink", - 0xFFFBB2A3: "Rose Bud", - 0xFFFBBEDA: "Cupid", - 0xFFFBCCE7: "Classic Rose", - 0xFFFBCEB1: "Apricot Peach", - 0xFFFBE7B2: "Banana Mania", - 0xFFFBE870: "Marigold Yellow", - 0xFFFBE96C: "Festival", - 0xFFFBEA8C: "Sweet Corn", - 0xFFFBEC5D: "Candy Corn", - 0xFFFBF9F9: "Hint of Red", - 0xFFFBFFBA: "Shalimar", - 0xFFFC0FC0: "Shocking Pink", - 0xFFFC80A5: "Tickle Me Pink", - 0xFFFC9C1D: "Tree Poppy", - 0xFFFCC01E: "Lightning Yellow", - 0xFFFCD667: "Goldenrod", - 0xFFFCD917: "Candlelight", - 0xFFFCDA98: "Cherokee", - 0xFFFCF4D0: "Double Pearl Lusta", - 0xFFFCF4DC: "Pearl Lusta", - 0xFFFCF8F7: "Vista White", - 0xFFFCFBF3: "Bianca", - 0xFFFCFEDA: "Moon Glow", - 0xFFFCFFE7: "China Ivory", - 0xFFFCFFF9: "Ceramic", - 0xFFFD0E35: "Torch Red", - 0xFFFD5B78: "Wild Watermelon", - 0xFFFD7B33: "Crusta", - 0xFFFD7C07: "Sorbus", - 0xFFFD9FA2: "Sweet Pink", - 0xFFFDD5B1: "Light Apricot", - 0xFFFDD7E4: "Pig Pink", - 0xFFFDE1DC: "Cinderella", - 0xFFFDE295: "Golden Glow", - 0xFFFDE910: "Lemon", + 0xFFFAFAD2: "Light Goldenrod Yellow", 0xFFFDF5E6: "Old Lace", - 0xFFFDF6D3: "Half Colonial White", - 0xFFFDF7AD: "Drover", - 0xFFFDFEB8: "Pale Prim", - 0xFFFDFFD5: "Cumulus", - 0xFFFE28A2: "Persian Rose", - 0xFFFE4C40: "Sunset Orange", - 0xFFFE6F5E: "Bittersweet", - 0xFFFE9D04: "California", - 0xFFFEA904: "Yellow Sea", - 0xFFFEBAAD: "Melon", - 0xFFFED33C: "Bright Sun", - 0xFFFED85D: "Dandelion", - 0xFFFEDB8D: "Salomie", - 0xFFFEE5AC: "Cape Honey", - 0xFFFEEBF3: "Remy", - 0xFFFEEFCE: "Oasis", - 0xFFFEF0EC: "Bridesmaid", - 0xFFFEF2C7: "Beeswax", - 0xFFFEF3D8: "Bleach White", - 0xFFFEF4CC: "Pipi", - 0xFFFEF4DB: "Half Spanish White", - 0xFFFEF4F8: "Wisp Pink", - 0xFFFEF5F1: "Provincial Pink", - 0xFFFEF7DE: "Half Dutch White", - 0xFFFEF8E2: "Solitaire", - 0xFFFEF8FF: "White Pointer", - 0xFFFEF9E3: "Off Yellow", - 0xFFFEFCED: "Orange White", 0xFFFF0000: "Red", - 0xFFFF007F: "Rose", - 0xFFFF00CC: "Purple Pizzazz", - 0xFFFF00FF: "Magenta / Fuchsia", - 0xFFFF2400: "Scarlet", - 0xFFFF3399: "Wild Strawberry", - 0xFFFF33CC: "Razzle Dazzle Rose", - 0xFFFF355E: "Radical Red", - 0xFFFF3F34: "Red Orange", - 0xFFFF4040: "Coral Red", - 0xFFFF4D00: "Vermilion", - 0xFFFF4F00: "International Orange", - 0xFFFF6037: "Outrageous Orange", - 0xFFFF6600: "Blaze Orange", - 0xFFFF66FF: "Pink Flamingo", - 0xFFFF681F: "Orange", + 0xFFFF00FF: "Magenta", + 0xFFFF1493: "Deep Pink", + 0xFFFF4500: "Orange Red", + 0xFFFF6347: "Tomato", 0xFFFF69B4: "Hot Pink", - 0xFFFF6B53: "Persimmon", - 0xFFFF6FFF: "Blush Pink", - 0xFFFF7034: "Burning Orange", - 0xFFFF7518: "Pumpkin", - 0xFFFF7D07: "Flamenco", - 0xFFFF7F00: "Flush Orange", 0xFFFF7F50: "Coral", - 0xFFFF8C69: "Salmon", - 0xFFFF9000: "Pizazz", - 0xFFFF910F: "West Side", - 0xFFFF91A4: "Pink Salmon", - 0xFFFF9933: "Neon Carrot", - 0xFFFF9966: "Atomic Tangerine", - 0xFFFF9980: "Vivid Tangerine", - 0xFFFF9E2C: "Sunshade", - 0xFFFFA000: "Orange Peel", - 0xFFFFA194: "Mona Lisa", - 0xFFFFA500: "Web Orange", - 0xFFFFA6C9: "Carnation Pink", - 0xFFFFAB81: "Hit Pink", - 0xFFFFAE42: "Yellow Orange", - 0xFFFFB0AC: "Cornflower Lilac", - 0xFFFFB1B3: "Sundown", - 0xFFFFB31F: "My Sin", - 0xFFFFB555: "Texas Rose", - 0xFFFFB7D5: "Cotton Candy", - 0xFFFFB97B: "Macaroni and Cheese", - 0xFFFFBA00: "Selective Yellow", - 0xFFFFBD5F: "Koromiko", - 0xFFFFBF00: "Amber", - 0xFFFFC0A8: "Wax Flower", + 0xFFFF8C00: "Dark Orange", + 0xFFFFA07A: "Light Salmon", + 0xFFFFA500: "Orange", + 0xFFFFB6C1: "Light Pink", 0xFFFFC0CB: "Pink", - 0xFFFFC3C0: "Your Pink", - 0xFFFFC901: "Supernova", - 0xFFFFCBA4: "Flesh", - 0xFFFFCC33: "Sunglow", - 0xFFFFCC5C: "Golden Tainoi", - 0xFFFFCC99: "Peach Orange", - 0xFFFFCD8C: "Chardonnay", - 0xFFFFD1DC: "Pastel Pink", - 0xFFFFD2B7: "Romantic", - 0xFFFFD38C: "Grandis", 0xFFFFD700: "Gold", - 0xFFFFD800: "School bus Yellow", - 0xFFFFD8D9: "Cosmos", - 0xFFFFDB58: "Mustard", - 0xFFFFDCD6: "Peach Schnapps", - 0xFFFFDDAF: "Caramel", - 0xFFFFDDCD: "Tuft Bush", - 0xFFFFDDCF: "Watusi", - 0xFFFFDDF4: "Pink Lace", + 0xFFFFDAB9: "Peach Puff", 0xFFFFDEAD: "Navajo White", - 0xFFFFDEB3: "Frangipani", - 0xFFFFE1DF: "Pippin", - 0xFFFFE1F2: "Pale Rose", - 0xFFFFE2C5: "Negroni", - 0xFFFFE5A0: "Cream Brulee", - 0xFFFFE5B4: "Peach", - 0xFFFFE6C7: "Tequila", - 0xFFFFE772: "Kournikova", - 0xFFFFEAC8: "Sandy Beach", - 0xFFFFEAD4: "Karry", - 0xFFFFEC13: "Broom", - 0xFFFFEDBC: "Colonial White", - 0xFFFFEED8: "Derby", - 0xFFFFEFA1: "Vis Vis", - 0xFFFFEFC1: "Egg White", + 0xFFFFE4B5: "Moccasin", + 0xFFFFE4C4: "Bisque", + 0xFFFFE4E1: "Misty Rose", + 0xFFFFEBCD: "Blanched Almond", 0xFFFFEFD5: "Papaya Whip", - 0xFFFFEFEC: "Fair Pink", - 0xFFFFF0DB: "Peach Cream", - 0xFFFFF0F5: "Lavender blush", - 0xFFFFF14F: "Gorse", - 0xFFFFF1B5: "Buttermilk", - 0xFFFFF1D8: "Pink Lady", - 0xFFFFF1EE: "Forget Me Not", - 0xFFFFF1F9: "Tutu", - 0xFFFFF39D: "Picasso", - 0xFFFFF3F1: "Chardon", - 0xFFFFF46E: "Paris Daisy", - 0xFFFFF4CE: "Barley White", - 0xFFFFF4DD: "Egg Sour", - 0xFFFFF4E0: "Sazerac", - 0xFFFFF4E8: "Serenade", - 0xFFFFF4F3: "Chablis", - 0xFFFFF5EE: "Seashell Peach", - 0xFFFFF5F3: "Sauvignon", - 0xFFFFF6D4: "Milk Punch", - 0xFFFFF6DF: "Varden", - 0xFFFFF6F5: "Rose White", - 0xFFFFF8D1: "Baja White", - 0xFFFFF9E2: "Gin Fizz", - 0xFFFFF9E6: "Early Dawn", + 0xFFFFF0F5: "Lavender Blush", + 0xFFFFF5EE: "Sea Shell", + 0xFFFFF8DC: "Cornsilk", 0xFFFFFACD: "Lemon Chiffon", - 0xFFFFFAF4: "Bridal Heath", - 0xFFFFFBDC: "Scotch Mist", - 0xFFFFFBF9: "Soapstone", - 0xFFFFFC99: "Witch Haze", - 0xFFFFFCEA: "Buttery White", - 0xFFFFFCEE: "Island Spice", - 0xFFFFFDD0: "Cream", - 0xFFFFFDE6: "Chilean Heath", - 0xFFFFFDE8: "Travertine", - 0xFFFFFDF3: "Orchid White", - 0xFFFFFDF4: "Quarter Pearl Lusta", - 0xFFFFFEE1: "Half and Half", - 0xFFFFFEEC: "Apricot White", - 0xFFFFFEF0: "Rice Cake", - 0xFFFFFEF6: "Black White", - 0xFFFFFEFD: "Romance", + 0xFFFFFAF0: "Floral White", + 0xFFFFFAFA: "Snow", 0xFFFFFF00: "Yellow", - 0xFFFFFF66: "Laser Lemon", - 0xFFFFFF99: "Pale Canary", - 0xFFFFFFB4: "Portafino", + 0xFFFFFFE0: "Light Yellow", 0xFFFFFFF0: "Ivory", 0xFFFFFFFF: "White", }; diff --git a/lib/src/controls/form/color_picker/color_picker.dart b/lib/src/controls/form/color_picker/color_picker.dart index f7c74032b..58b8a424d 100644 --- a/lib/src/controls/form/color_picker/color_picker.dart +++ b/lib/src/controls/form/color_picker/color_picker.dart @@ -17,7 +17,7 @@ enum ColorSpectrumShape { } /// Defines the color mode used in the [ColorPicker]. -enum ColorMode { +enum _ColorMode { /// RGB (Red, Green, Blue) color mode. rgb, @@ -61,41 +61,27 @@ enum _ColorPickerSizes { spectrum.size + _ColorPickerSpacing.small.size + preview.size; } -/// Color Picker +/// A color picker is used to browse through and select colors. +/// By default, it lets a user navigate through colors on a color +/// spectrum, or specify a color in either Red-Green-Blue (RGB), +/// Hue-Saturation-Value (HSV), or Hexadecimal text boxes. /// -/// A comprehensive color picker implementation that supports both RGB and HSV color models, -/// with wheel and box spectrum shapes. Integrates with Fluent UI's theming system for -/// consistent look and feel. +/// ![ColorPicker Preview](https://learn.microsoft.com/en-us/windows/apps/design/controls/images/color-picker-default.png) /// -/// Features: -/// - Color wheel and box spectrum modes -/// - RGB and HSV color input modes -/// - Alpha channel support -/// - Hex color input -/// - Real-time color name display -/// - Theme-aware tooltips and UI elements -/// - Value and alpha sliders +/// See also: /// -/// Example usage: -/// ```dart -/// ColorPicker( -/// value: Colors.blue, -/// onChanged: (Color color) { -/// setState(() { -/// _selectedColor = color; -/// }); -/// }, -/// colorSpectrumShape: ColorSpectrumShape.ring, -/// ) -/// ``` +/// * [ColorSpectrumShape], which defines the shape of the color spectrum. +/// * class ColorPicker extends StatefulWidget { /// The current color value final Color color; - /// Callback when the color value changes + /// Called when the color value changes. final ValueChanged onChanged; /// The orientation of the color picker layout + /// + /// Defaults to [Axis.vertical]. final Axis orientation; /// Whether the color preview is visible @@ -122,7 +108,9 @@ class ColorPicker extends StatefulWidget { /// Whether the alpha text input is visible final bool isAlphaTextInputVisible; - /// The shape of the color spectrum (ring or box) + /// The shape of the color spectrum. + /// + /// Defaults to [ColorSpectrumShape.ring]. final ColorSpectrumShape colorSpectrumShape; /// The minimum allowed hue value (0-359) @@ -143,26 +131,7 @@ class ColorPicker extends StatefulWidget { /// The maximum allowed value/brightness (0-100) final int maxValue; - /// Creates a new instance of [ColorPicker]. - /// - /// - [color]: The current color value. - /// - [onChanged]: Callback when the color value changes. - /// - [orientation]: The orientation of the color picker layout. Defaults to [Axis.vertical]. - /// - [colorSpectrumShape]: The shape of the color spectrum (ring or box). Defaults to [ColorSpectrumShape.ring]. - /// - [isColorPreviewVisible]: Whether the color preview is visible. Defaults to true. - /// - [isColorSliderVisible]: Whether the color slider is visible. Defaults to true. - /// - [isMoreButtonVisible]: Whether the "More" button is visible. Defaults to true. - /// - [isHexInputVisible]: Whether the hex input is visible. Defaults to true. - /// - [isColorChannelTextInputVisible]: Whether the color channel text input is visible. Defaults to true. - /// - [isAlphaEnabled]: Whether the alpha channel is enabled. Defaults to true. - /// - [isAlphaSliderVisible]: Whether the alpha slider is visible. Defaults to true. - /// - [isAlphaTextInputVisible]: Whether the alpha text input is visible. Defaults to true. - /// - [minHue]: The minimum allowed hue value (0-359). Defaults to 0. - /// - [maxHue]: The maximum allowed hue value (0-359). Defaults to 359. - /// - [minSaturation]: The minimum allowed saturation value (0-100). Defaults to 0. - /// - [maxSaturation]: The maximum allowed saturation value (0-100). Defaults to 100. - /// - [minValue]: The minimum allowed value/brightness (0-100). Defaults to 0. - /// - [maxValue]: The maximum allowed value/brightness (0-100). Defaults to 100. + /// Creates a fluent-styled [ColorPicker]. const ColorPicker({ super.key, required this.color, @@ -691,8 +660,11 @@ class _ColorSliders extends StatelessWidget { Widget _buildValueSlider(FluentThemeData theme, bool isVertical) { final thumbColor = theme.resources.focusStrokeColorOuter; final colorName = colorState.guessColorName(); + + // Format the value text with color name + // TODO: Add localization support for "Value" text final valueText = - '${(colorState.value * 100).round()}% ${colorName.isNotEmpty ? "($colorName)" : ""}'; + 'Value ${(colorState.value * 100).round()}${colorName.isNotEmpty ? " ($colorName)" : ""}'; return SizedBox( width: isVertical @@ -803,13 +775,13 @@ class _ColorSliders extends StatelessWidget { /// A widget that displays the color inputs. class _ColorInputs extends StatelessWidget { /// Map of color modes (RGB and HSV) - static const Map colorModes = { - 'RGB': ColorMode.rgb, - 'HSV': ColorMode.hsv, + static const Map colorModes = { + 'RGB': _ColorMode.rgb, + 'HSV': _ColorMode.hsv, }; /// Internal ValueNotifier for color mode management - static final _colorModeNotifier = ValueNotifier(ColorMode.rgb); + static final _colorModeNotifier = ValueNotifier<_ColorMode>(_ColorMode.rgb); /// The current color state final ColorState colorState; @@ -884,7 +856,7 @@ class _ColorInputs extends StatelessWidget { // Update hex input whenever colorState changes _updateHexControllerText(); - return ValueListenableBuilder( + return ValueListenableBuilder<_ColorMode>( valueListenable: _colorModeNotifier, builder: (context, colorMode, _) { final inputsContent = Column( @@ -895,7 +867,7 @@ class _ColorInputs extends StatelessWidget { !isMoreButtonVisible || isMoreExpanded) ...[ _buildColorModeAndHexInput(colorMode), - colorMode == ColorMode.rgb + colorMode == _ColorMode.rgb ? _buildRGBInputs() : _buildHSVInputs(), ], @@ -931,10 +903,10 @@ class _ColorInputs extends StatelessWidget { } /// Builds the color mode selector and hex input. - Widget _buildColorModeAndHexInput(ColorMode colorMode) { + Widget _buildColorModeAndHexInput(_ColorMode colorMode) { final modeSelector = SizedBox( width: _ColorPickerSizes.inputBox.size, - child: ComboBox( + child: ComboBox<_ColorMode>( value: colorMode, items: colorModes.entries .map((e) => ComboBoxItem(value: e.value, child: Text(e.key))) @@ -1160,12 +1132,13 @@ class _ColorInputs extends StatelessWidget { final r = ((colorValue >> 16) & 0xFF) / 255.0; final g = ((colorValue >> 8) & 0xFF) / 255.0; final b = (colorValue & 0xFF) / 255.0; + final rgb = RgbComponents(r, g, b); // Convert RGB to HSV - final (h, s, v) = ColorState.rgbToHsv(r, g, b); + final hsv = ColorState.rgbToHsv(rgb); // Create new ColorState - final newState = ColorState(r, g, b, a, h, s, v); + final newState = ColorState(r, g, b, a, hsv.h, hsv.s, hsv.v); onColorChanged(newState); } catch (e) { debugPrint('Error parsing hex color: $e'); diff --git a/lib/src/controls/form/color_picker/color_spectrum.dart b/lib/src/controls/form/color_picker/color_spectrum.dart index 462c4a7cd..60671c123 100644 --- a/lib/src/controls/form/color_picker/color_spectrum.dart +++ b/lib/src/controls/form/color_picker/color_spectrum.dart @@ -378,28 +378,34 @@ class _RingSpectrumPainter extends CustomPainter { // Draw indicator with current color and border // Calculate perceived brightness to determine stroke color - final rgb = ColorState.hsvToRgb(colorState.hue, colorState.saturation, 1.0); - final fillColor = Color.fromARGB(255, (rgb.$1 * 255).round(), - (rgb.$2 * 255).round(), (rgb.$3 * 255).round()); - final double brightness = 0.299 * rgb.$1 + 0.587 * rgb.$2 + 0.114 * rgb.$3; - final strokeColor = brightness > 0.5 ? Colors.black : Colors.white; + final rgb = ColorState.hsvToRgb( + HsvComponents(colorState.hue, colorState.saturation, 1.0)); + final fillColor = Color.fromARGB(255, (rgb.r * 255).round(), + (rgb.g * 255).round(), (rgb.b * 255).round()); + + // Compute relative luminance to determine optimal stroke color visibility + final relativeLuminance = ColorState.relativeLuminance(fillColor); + final brightness = (relativeLuminance + 0.05) * (relativeLuminance + 0.05); + + // Choose stroke color based on background brightness + // Threshold: 0.30 (based on WinUI 3 ColorPicker testing) + // - Above threshold: black stroke for light backgrounds + // - Below threshold: white stroke for dark backgrounds + // Reference: + // - Flutter Material uses 0.15 threshold (https://api.flutter.dev/flutter/material/ThemeData/estimateBrightnessForColor.html) + // - This implementation follows WinUI 3's 0.30 threshold + final strokeColor = brightness > 0.30 ? Colors.black : Colors.white; // Draw white circle with black border for indicator canvas.drawCircle( indicatorOffset, - 8, + 7.5, Paint() ..color = strokeColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); - canvas.drawCircle( - indicatorOffset, - 7, - Paint()..color = fillColor, - ); - // Draw color name label if needed if (showLabel) { final colorName = colorState.guessColorName(); @@ -592,28 +598,34 @@ class _BoxSpectrumPainter extends CustomPainter { // Draw indicator with current color and white border // Calculate perceived brightness to determine stroke color - final rgb = ColorState.hsvToRgb(colorState.hue, colorState.saturation, 1.0); - final fillColor = Color.fromARGB(255, (rgb.$1 * 255).round(), - (rgb.$2 * 255).round(), (rgb.$3 * 255).round()); - final double brightness = 0.299 * rgb.$1 + 0.587 * rgb.$2 + 0.114 * rgb.$3; - final strokeColor = brightness > 0.5 ? Colors.black : Colors.white; + final rgb = ColorState.hsvToRgb( + HsvComponents(colorState.hue, colorState.saturation, 1.0)); + final fillColor = Color.fromARGB(255, (rgb.r * 255).round(), + (rgb.g * 255).round(), (rgb.b * 255).round()); + + // Compute relative luminance to determine optimal stroke color visibility + final relativeLuminance = ColorState.relativeLuminance(fillColor); + final brightness = (relativeLuminance + 0.05) * (relativeLuminance + 0.05); + + // Choose stroke color based on background brightness + // Threshold: 0.30 (based on WinUI 3 ColorPicker testing) + // - Above threshold: black stroke for light backgrounds + // - Below threshold: white stroke for dark backgrounds + // Reference: + // - Flutter Material uses 0.15 threshold (https://api.flutter.dev/flutter/material/ThemeData/estimateBrightnessForColor.html) + // - This implementation follows WinUI 3's 0.30 threshold + final strokeColor = brightness > 0.30 ? Colors.black : Colors.white; // Draw white circle with black border for indicator canvas.drawCircle( Offset(x, y), - 8, + 7.5, Paint() ..color = strokeColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); - canvas.drawCircle( - Offset(x, y), - 7, - Paint()..color = fillColor, - ); - // Draw color name label if needed if (showLabel) { final colorName = colorState.guessColorName(); @@ -712,7 +724,7 @@ class _BoxSpectrumPainter extends CustomPainter { /// Custom painter for drawing a checkerboard pattern. /// -/// This painter is used to represent transparency in the [ColorPicker]'s alpha slider. +/// This painter is used to represent transparency in the [ColorPicker]'s alpha slider and preview box. class CheckerboardPainter extends CustomPainter { /// The theme data for styling final FluentThemeData theme; @@ -749,5 +761,6 @@ class CheckerboardPainter extends CustomPainter { } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + bool shouldRepaint(covariant CheckerboardPainter oldDelegate) => + theme != oldDelegate.theme; } diff --git a/lib/src/controls/form/color_picker/color_state.dart b/lib/src/controls/form/color_picker/color_state.dart index bfd92172d..e488f63b2 100644 --- a/lib/src/controls/form/color_picker/color_state.dart +++ b/lib/src/controls/form/color_picker/color_state.dart @@ -4,6 +4,30 @@ import 'package:fluent_ui/fluent_ui.dart'; part "color_names.dart"; +/// Represents components of a color in the HSV (Hue, Saturation, Value) color space. +final class HsvComponents { + const HsvComponents(this.h, this.s, this.v); + final double h; + final double s; + final double v; +} + +/// Represents components of a color in the HSL (Hue, Saturation, Lightness) color space. +final class HslComponents { + const HslComponents(this.h, this.s, this.l); + final double h; + final double s; + final double l; +} + +/// Represents components of a color in the RGB (Red, Green, Blue) color space. +final class RgbComponents { + const RgbComponents(this.r, this.g, this.b); + final double r; + final double g; + final double b; +} + /// A stateful representation of a color in both RGB and HSV color spaces. class ColorState extends ChangeNotifier { // RGB components (0-1) @@ -55,8 +79,8 @@ class ColorState extends ChangeNotifier { final b = color.blue.toDouble() / 255; final a = color.alpha.toDouble() / 255; - final (h, s, v) = rgbToHsv(r, g, b); - return ColorState(r, g, b, a, h, s, v); + final hsv = rgbToHsv(RgbComponents(r, g, b)); + return ColorState(r, g, b, a, hsv.h, hsv.s, hsv.v); } /// Sets the hue and updates the RGB values accordingly. @@ -82,10 +106,10 @@ class ColorState extends ChangeNotifier { _green = 0; _blue = 0; } else { - final (r, g, b) = hsvToRgb(_hue, _saturation, _value); - _red = r; - _green = g; - _blue = b; + final rgb = hsvToRgb(HsvComponents(_hue, _saturation, _value)); + _red = rgb.r; + _green = rgb.g; + _blue = rgb.b; } notifyListeners(); @@ -132,10 +156,10 @@ class ColorState extends ChangeNotifier { _blue = color.blue / 255; _alpha = color.alpha / 255; - final (h, s, v) = rgbToHsv(_red, _green, _blue); - _hue = h; - _saturation = s; - _value = v; + final hsv = rgbToHsv(RgbComponents(_red, _green, _blue)); + _hue = hsv.h; + _saturation = hsv.s; + _value = hsv.v; notifyListeners(); } catch (e) { @@ -160,8 +184,8 @@ class ColorState extends ChangeNotifier { /// Guess the name of the color based on the current RGB values. String guessColorName() { try { - final rgb1 = (red: _red, green: _green, blue: _blue); - final hsl1 = rgbToHsl(_red, _green, _blue); + final rgb1 = RgbComponents(_red, _green, _blue); + final hsl1 = rgbToHsl(rgb1); double minDistance = double.infinity; String closestColorName = ''; @@ -174,8 +198,8 @@ class ColorState extends ChangeNotifier { final g = ((hexColor >> 8) & 0xFF) / 255.0; final b = (hexColor & 0xFF) / 255.0; - final rgb2 = (red: r, green: g, blue: b); - final hsl2 = rgbToHsl(r, g, b); + final rgb2 = RgbComponents(r, g, b); + final hsl2 = rgbToHsl(rgb2); final distance = _rgbHslDistance(rgb1, hsl1, rgb2, hsl2); if (distance < minDistance) { @@ -256,19 +280,19 @@ class ColorState extends ChangeNotifier { final scaledG = _green / v; final scaledB = _blue / v; - final (h, s, tempV) = rgbToHsv(scaledR, scaledG, scaledB); - _hue = h; - _saturation = s; + final hsv = rgbToHsv(RgbComponents(scaledR, scaledG, scaledB)); + _hue = hsv.h; + _saturation = hsv.s; } _value = v; } /// Updates the RGB values based on the current HSV values. void _recalculateRGBFromHSV() { - final (r, g, b) = hsvToRgb(_hue, _saturation, _value); - _red = r; - _green = g; - _blue = b; + final rgb = hsvToRgb(HsvComponents(_hue, _saturation, _value)); + _red = rgb.r; + _green = rgb.g; + _blue = rgb.b; } /// Validates that all color values are within their valid ranges. @@ -317,54 +341,54 @@ class ColorState extends ChangeNotifier { } /// Converts Color to HSV. - static (double h, double s, double l) colorToHsv(Color color) { + static HsvComponents colorToHsv(Color color) { final red = color.red / 255; final green = color.green / 255; final blue = color.blue / 255; - return rgbToHsv(red, green, blue); + return rgbToHsv(RgbComponents(red, green, blue)); } /// Converts Color to HSL. - static (double h, double s, double l) colorToHsl(Color color) { + static HslComponents colorToHsl(Color color) { final red = color.red / 255; final green = color.green / 255; final blue = color.blue / 255; - return rgbToHsl(red, green, blue); + return rgbToHsl(RgbComponents(red, green, blue)); } /// Converts RGB values to HSV. - static (double h, double s, double v) rgbToHsv(double r, double g, double b) { - final min = math.min(r, math.min(g, b)); - final max = math.max(r, math.max(g, b)); + static HsvComponents rgbToHsv(RgbComponents rgb) { + final min = math.min(rgb.r, math.min(rgb.g, rgb.b)); + final max = math.max(rgb.r, math.max(rgb.g, rgb.b)); final delta = max - min; final v = max; final s = max == 0 ? 0.0 : delta / max; if (delta == 0) { - return (0, s, v); + return HsvComponents(0, s, v); } double h; - if (max == r) { - h = (g - b) / delta; - } else if (max == g) { - h = 2 + (b - r) / delta; + if (max == rgb.r) { + h = (rgb.g - rgb.b) / delta; + } else if (max == rgb.g) { + h = 2 + (rgb.b - rgb.r) / delta; } else { - h = 4 + (r - g) / delta; + h = 4 + (rgb.r - rgb.g) / delta; } h *= 60; if (h < 0) h += 360; - return (h, s, v); + return HsvComponents(h, s, v); } /// Converts RGB values to HSL. - static (double h, double s, double l) rgbToHsl(double r, double g, double b) { - final max = math.max(r, math.max(g, b)); - final min = math.min(r, math.min(g, b)); + static HslComponents rgbToHsl(RgbComponents rgb) { + final max = math.max(rgb.r, math.max(rgb.g, rgb.b)); + final min = math.min(rgb.r, math.min(rgb.g, rgb.b)); final delta = max - min; // Calculate lightness @@ -378,76 +402,80 @@ class ColorState extends ChangeNotifier { if (delta == 0) { h = 0.0; // achromatic (gray) } else { - if (max == r) { - h = ((g - b) / delta) % 6; - } else if (max == g) { - h = (b - r) / delta + 2.0; + if (max == rgb.r) { + h = ((rgb.g - rgb.b) / delta) % 6; + } else if (max == rgb.g) { + h = (rgb.b - rgb.r) / delta + 2.0; } else { // max == b - h = (r - g) / delta + 4.0; + h = (rgb.r - rgb.g) / delta + 4.0; } h *= 60; if (h < 0) h += 360; } - return (h, s, l); + return HslComponents(h, s, l); } /// Converts HSV values to RGB. - static (double r, double g, double b) hsvToRgb(double h, double s, double v) { - if (s <= 0) return (v, v, v); // achromatic (grey) + static RgbComponents hsvToRgb(HsvComponents hsv) { + if (hsv.s <= 0) { + return RgbComponents(hsv.v, hsv.v, hsv.v); // achromatic (grey) + } - h = (h % 360) / 60; // Normalize hue - final i = h.floor(); - final f = h - i.toDouble(); - final p = v * (1.0 - s); - final q = v * (1.0 - s * f); - final t = v * (1.0 - s * (1.0 - f)); + final angle = (hsv.h % 360) / 60; // Normalize hue + final i = angle.floor(); + final f = angle - i.toDouble(); + final p = hsv.v * (1.0 - hsv.s); + final q = hsv.v * (1.0 - hsv.s * f); + final t = hsv.v * (1.0 - hsv.s * (1.0 - f)); switch (i % 6) { case 0: // 0 <= h < 60 - return (v, t, p); + return RgbComponents(hsv.v, t, p); case 1: - return (q, v, p); + return RgbComponents(q, hsv.v, p); case 2: - return (p, v, t); + return RgbComponents(p, hsv.v, t); case 3: - return (p, q, v); + return RgbComponents(p, q, hsv.v); case 4: - return (t, p, v); + return RgbComponents(t, p, hsv.v); default: - return (v, p, q); + return RgbComponents(hsv.v, p, q); } } /// Converts HSL values to RGB. - static (double r, double g, double b) hslToRgb(double h, double s, double l) { - if (s == 0) return (l, l, l); // achromatic (grey) + static RgbComponents hslToRgb(HslComponents hsl) { + if (hsl.s == 0) { + return RgbComponents(hsl.l, hsl.l, hsl.l); // achromatic (grey) + } - final q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; - final p = 2.0 * l - q; - final r = _hueToRgb(p, q, h + 1.0 / 3.0); - final g = _hueToRgb(p, q, h); - final b = _hueToRgb(p, q, h - 1.0 / 3.0); + final q = + hsl.l < 0.5 ? hsl.l * (1.0 + hsl.s) : hsl.l + hsl.s - hsl.l * hsl.s; + final p = 2.0 * hsl.l - q; + final r = _hueToRgb(p, q, hsl.h + 1.0 / 3.0); + final g = _hueToRgb(p, q, hsl.h); + final b = _hueToRgb(p, q, hsl.h - 1.0 / 3.0); - return (r, g, b); + return RgbComponents(r, g, b); } /// Converts HSV values to HSL. - static (double h, double s, double l) hsvToHsl(double h, double s, double v) { - final hslH = h; - final hslL = v - v * s / 2.0; - final hslS = - (hslL == 0 || hslL == 1) ? 0.0 : (v - hslL) / math.min(hslL, 1 - hslL); - return (hslH, hslS, hslL); + static HslComponents hsvToHsl(HsvComponents hsv) { + final h = hsv.h; + final l = hsv.v - hsv.v * hsv.s / 2.0; + final s = (l == 0 || l == 1) ? 0.0 : (hsv.v - l) / math.min(l, 1 - l); + return HslComponents(h, s, l); } /// Converts HSL values to HSV. - static (double h, double s, double v) hslToHsv(double h, double s, double l) { - final hsvH = h; - final hsvV = l + s * math.min(l, 1 - l); - final hsvS = hsvV == 0 ? 0.0 : 2 * (1 - l / hsvV); - return (hsvH, hsvS, hsvV); + static HsvComponents hslToHsv(HslComponents hsl) { + final h = hsl.h; + final v = hsl.l + hsl.s * math.min(hsl.l, 1 - hsl.l); + final s = v == 0 ? 0.0 : 2 * (1 - hsl.l / v); + return HsvComponents(h, s, v); } /// Helper method for HSL to RGB conversion. @@ -473,13 +501,12 @@ class ColorState extends ChangeNotifier { static double colorDistance(Color from, Color to) { // Normalize RGB values to range 0-1 final fromRgb = - (red: from.red / 255, green: from.green / 255, blue: from.blue / 255); - final toRgb = - (red: to.red / 255, green: to.green / 255, blue: to.blue / 255); + RgbComponents(from.red / 255, from.green / 255, from.blue / 255); + final toRgb = RgbComponents(to.red / 255, to.green / 255, to.blue / 255); // Convert RGB to HSL - final fromHsl = rgbToHsl(fromRgb.red, fromRgb.green, fromRgb.blue); - final toHsl = rgbToHsl(toRgb.red, toRgb.green, toRgb.blue); + final fromHsl = rgbToHsl(fromRgb); + final toHsl = rgbToHsl(toRgb); // Calculate distance using _distanceBetween return _rgbHslDistance(fromRgb, fromHsl, toRgb, toHsl); @@ -488,33 +515,63 @@ class ColorState extends ChangeNotifier { /// Get distance between two ColorState objects considering both RGB and HSL spaces static double colorStateDistance(ColorState from, ColorState to) { // Extract RGB values from ColorState objects - final fromRgb = (red: from.red, green: from.green, blue: from.blue); - final toRgb = (red: to.red, green: to.green, blue: to.blue); + final fromRgb = RgbComponents(from.red, from.green, from.blue); + final toRgb = RgbComponents(to.red, to.green, to.blue); // Extract HSL values from ColorState objects - final fromHsl = rgbToHsl(from.red, from.green, from.blue); - final toHsl = rgbToHsl(to.red, to.green, to.blue); + final fromHsl = rgbToHsl(fromRgb); + final toHsl = rgbToHsl(toRgb); // Calculate distance using _distanceBetween method return _rgbHslDistance(fromRgb, fromHsl, toRgb, toHsl); } /// Distance calculation between two colors in RGB and HSL spaces. - static double _rgbHslDistance( - ({double red, double green, double blue}) rgb1, - (double h, double s, double l) hsl1, - ({double red, double green, double blue}) rgb2, - (double h, double s, double l) hsl2) { + static double _rgbHslDistance(RgbComponents rgb1, HslComponents hsl1, + RgbComponents rgb2, HslComponents hsl2) { + final (r1, g1, b1) = (rgb1.r, rgb1.g, rgb1.b); + final (h1, s1, l1) = (hsl1.h, hsl1.s, hsl1.l); + final (r2, g2, b2) = (rgb2.r, rgb2.g, rgb2.b); + final (h2, s2, l2) = (hsl2.h, hsl2.s, hsl2.l); + // RGB distance = (R1 - R2)^2 + (G1 - G2)^2 + (B1 - B2)^2 - final rgbDiff = math.pow((rgb1.red - rgb2.red) * 255, 2) + - math.pow((rgb1.green - rgb2.green) * 255, 2) + - math.pow((rgb1.blue - rgb2.blue) * 255, 2); + final rgbDiff = math.pow((r1 - r2) * 255, 2) + + math.pow((g1 - g2) * 255, 2) + + math.pow((b1 - b2) * 255, 2); // HSL distance = ((H1 - H2)/360)^2 + (S1 - S2)^2 + (L1 - L2)^2 - final hslDiff = math.pow((hsl1.$1 - hsl2.$1) / 360, 2) + - math.pow(hsl1.$2 - hsl2.$2, 2) + - math.pow(hsl1.$3 - hsl2.$3, 2); + final hslDiff = math.pow((h1 - h2) / 360, 2) + + math.pow(s1 - s2, 2) + + math.pow(l1 - l2, 2); return rgbDiff + (hslDiff * 2); } + + /// Calculates the relative luminance of a color, representing its perceived brightness + /// to the human eye. + /// + /// The human eye has different sensitivities to different colors. For example, + /// blue (0, 0, 255) appears much darker than green (0, 255, 0) even though they + /// have the same numeric intensity. + /// + /// Returns a value between 0 (darkest) and 1 (brightest). + static double relativeLuminance(Color color) { + final r = _standardToLinear(color.red / 255); + final g = _standardToLinear(color.green / 255); + final b = _standardToLinear(color.blue / 255); + return (r * 0.2126 + g * 0.7152 + b * 0.0722); + } + + /// Converts a standard RGB color component to linear RGB color space. + /// + /// References: + /// - sRGB: https://en.wikipedia.org/wiki/SRGB + /// - WCAG 2.0: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + static double _standardToLinear(double c) { + // https://en.wikipedia.org/wiki/SRGB + // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + return c <= 0.03928 + ? c / 12.92 + : math.pow((c + 0.055) / 1.055, 2.4).toDouble(); + } }