creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/canvas/advanced_color_picker.dart (21441B)
1import 'package:flutter/material.dart';
2import 'package:flutter_colorpicker/flutter_colorpicker.dart';
3import 'package:creekui/ui/styles/variables.dart';
4
5class AdvancedColorPickerSheet extends StatefulWidget {
6 final Color initialColor;
7 final ValueChanged<Color> onColorChanged;
8 final List<Color> recentColors;
9 final List<Color> brandColors;
10
11 const AdvancedColorPickerSheet({
12 super.key,
13 required this.initialColor,
14 required this.onColorChanged,
15 required this.recentColors,
16 required this.brandColors,
17 });
18
19 @override
20 State<AdvancedColorPickerSheet> createState() =>
21 _AdvancedColorPickerSheetState();
22}
23
24class _AdvancedColorPickerSheetState extends State<AdvancedColorPickerSheet>
25 with SingleTickerProviderStateMixin {
26 late TabController _tabController;
27 late Color _currentColor;
28 late HSVColor _currentHsv;
29 late List<Color> _brandPalette;
30
31 final TextEditingController _rController = TextEditingController();
32 final TextEditingController _gController = TextEditingController();
33 final TextEditingController _bController = TextEditingController();
34
35 @override
36 void initState() {
37 super.initState();
38 _currentColor = widget.initialColor;
39 _currentHsv = HSVColor.fromColor(widget.initialColor);
40 _tabController = TabController(length: 3, vsync: this);
41 _brandPalette = widget.brandColors;
42 _updateControllers();
43 }
44
45 void _updateControllers() {
46 _rController.text = _currentColor.red.toString();
47 _gController.text = _currentColor.green.toString();
48 _bController.text = _currentColor.blue.toString();
49 }
50
51 @override
52 void dispose() {
53 _tabController.dispose();
54 _rController.dispose();
55 _gController.dispose();
56 _bController.dispose();
57 super.dispose();
58 }
59
60 // Updates the active color and canvas immediately (Live Preview)
61 void _handleColorChange(Color color) {
62 setState(() {
63 _currentColor = color;
64 _currentHsv = HSVColor.fromColor(color);
65 _updateControllers();
66 });
67 widget.onColorChanged(color);
68 }
69
70 // Handler for Spectrum Tab to avoid HSV->RGB->HSV conversion loss
71 void _handleHsvChange(HSVColor hsv) {
72 setState(() {
73 _currentHsv = hsv;
74 _currentColor = hsv.toColor();
75 _updateControllers();
76 });
77 widget.onColorChanged(_currentColor);
78 }
79
80 // Saves the current color to the "Recently Used" list
81 void _saveToRecent() {
82 setState(() {
83 widget.recentColors.removeWhere((c) => c.value == _currentColor.value);
84 widget.recentColors.insert(0, _currentColor);
85 if (widget.recentColors.length > 5) {
86 widget.recentColors.removeLast();
87 }
88 });
89 }
90
91 @override
92 Widget build(BuildContext context) {
93 return Container(
94 height: MediaQuery.of(context).size.height * 0.85,
95 decoration: const BoxDecoration(
96 color: Colors.white,
97 borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
98 ),
99 child: Column(
100 crossAxisAlignment: CrossAxisAlignment.stretch,
101 children: [
102 const SizedBox(height: 12),
103 Center(
104 child: Container(
105 width: 40,
106 height: 4,
107 decoration: BoxDecoration(
108 color: Colors.grey[300],
109 borderRadius: BorderRadius.circular(2),
110 ),
111 ),
112 ),
113 const SizedBox(height: 16),
114 TabBar(
115 controller: _tabController,
116 labelColor: Colors.black,
117 unselectedLabelColor: Colors.grey,
118 indicatorColor: Colors.black,
119 indicatorSize: TabBarIndicatorSize.label,
120 labelStyle: const TextStyle(
121 fontWeight: FontWeight.w600,
122 fontSize: 14,
123 ),
124 tabs: const [
125 Tab(text: 'Grid'),
126 Tab(text: 'Spectrum'),
127 Tab(text: 'Sliders'),
128 ],
129 ),
130 const SizedBox(height: 16),
131 // Header: Hex Code & Preview
132 Padding(
133 padding: const EdgeInsets.symmetric(horizontal: 16),
134 child: Row(
135 children: [
136 Container(
137 padding: const EdgeInsets.symmetric(
138 horizontal: 8,
139 vertical: 6,
140 ),
141 decoration: BoxDecoration(
142 border: Border.all(color: Colors.grey.shade300),
143 borderRadius: BorderRadius.circular(8),
144 ),
145 child: Row(
146 mainAxisSize: MainAxisSize.min,
147 children: const [
148 Text(
149 "Hex",
150 style: TextStyle(
151 fontSize: 13,
152 fontWeight: FontWeight.bold,
153 ),
154 ),
155 SizedBox(width: 4),
156 Icon(
157 Icons.keyboard_arrow_down,
158 size: 16,
159 color: Colors.grey,
160 ),
161 ],
162 ),
163 ),
164 const SizedBox(width: 8),
165 Container(
166 width: 32,
167 height: 32,
168 decoration: BoxDecoration(
169 color: _currentColor,
170 borderRadius: BorderRadius.circular(6),
171 border: Border.all(color: Colors.grey.shade200),
172 ),
173 ),
174 const SizedBox(width: 8),
175 Expanded(
176 child: Container(
177 padding: const EdgeInsets.symmetric(
178 horizontal: 10,
179 vertical: 8,
180 ),
181 decoration: BoxDecoration(
182 color: Colors.grey.shade50,
183 borderRadius: BorderRadius.circular(8),
184 ),
185 child: Text(
186 "#${_currentColor.value.toRadixString(16).toUpperCase().substring(2)}",
187 style: const TextStyle(
188 fontWeight: FontWeight.w600,
189 fontSize: 14,
190 ),
191 overflow: TextOverflow.ellipsis,
192 ),
193 ),
194 ),
195 const SizedBox(width: 8),
196 const Icon(Icons.colorize, size: 20, color: Colors.black54),
197 ],
198 ),
199 ),
200 const SizedBox(height: 16),
201 const Divider(height: 1),
202 // Tab Views
203 Expanded(
204 child: TabBarView(
205 controller: _tabController,
206 physics: const NeverScrollableScrollPhysics(),
207 children: [
208 _buildGridTab(),
209 _buildSpectrumTab(),
210 _buildSlidersTab(),
211 ],
212 ),
213 ),
214 _buildSharedFooter(),
215 ],
216 ),
217 );
218 }
219
220 // Tab 1: Grid
221 List<Color> _generateColorGrid() {
222 List<Color> colors = [];
223 for (int i = 0; i < 9; i++) {
224 double lightness = 1.0 - (i / 8);
225 colors.add(HSLColor.fromAHSL(1.0, 0.0, 0.0, lightness).toColor());
226 }
227 final int hueSteps = 9;
228 final int shadeSteps = 7;
229 for (int shade = 0; shade < shadeSteps; shade++) {
230 for (int hueStep = 0; hueStep < hueSteps; hueStep++) {
231 double hue = (hueStep / hueSteps) * 360;
232 double saturation = 0.5 + (shade / shadeSteps) * 0.5;
233 double lightness = 0.8 - (shade / shadeSteps) * 0.5;
234 colors.add(
235 HSLColor.fromAHSL(1.0, hue, saturation, lightness).toColor(),
236 );
237 }
238 }
239 return colors;
240 }
241
242 Widget _buildGridTab() {
243 final gridColors = _generateColorGrid();
244
245 return Column(
246 children: [
247 Expanded(
248 child: Padding(
249 padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
250 child: GridView.builder(
251 physics: const BouncingScrollPhysics(),
252 gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
253 crossAxisCount: 9,
254 crossAxisSpacing: 0,
255 mainAxisSpacing: 0,
256 childAspectRatio: 1.0,
257 ),
258 itemCount: gridColors.length,
259 itemBuilder: (context, index) {
260 final color = gridColors[index];
261 final isSelected = _currentColor.value == color.value;
262 return GestureDetector(
263 onTap: () {
264 _handleColorChange(color);
265 _saveToRecent(); // Save immediately on tap
266 },
267 child: Stack(
268 alignment: Alignment.center,
269 children: [
270 Container(
271 decoration: BoxDecoration(
272 color: color,
273 border: Border.all(
274 color: Colors.black.withOpacity(0.05),
275 width: 0.5,
276 ),
277 ),
278 ),
279 if (isSelected)
280 Container(
281 width: 24,
282 height: 24,
283 decoration: BoxDecoration(
284 color: color,
285 shape: BoxShape.circle,
286 border: Border.all(color: Colors.white, width: 3),
287 boxShadow: [
288 BoxShadow(
289 color: Colors.black.withOpacity(0.2),
290 blurRadius: 4,
291 spreadRadius: 1,
292 ),
293 ],
294 ),
295 ),
296 ],
297 ),
298 );
299 },
300 ),
301 ),
302 ),
303 ],
304 );
305 }
306
307 // Tab 2: Spectrum
308 Widget _buildSpectrumTab() {
309 return Padding(
310 padding: const EdgeInsets.all(20),
311 child: Column(
312 children: [
313 // Saturation / Value Box
314 Expanded(
315 child: ClipRRect(
316 borderRadius: BorderRadius.circular(12),
317 child: LayoutBuilder(
318 builder: (context, constraints) {
319 return GestureDetector(
320 onPanUpdate: (details) {
321 RenderBox box = context.findRenderObject() as RenderBox;
322 Offset localOffset = box.globalToLocal(
323 details.globalPosition,
324 );
325
326 double saturation = (localOffset.dx /
327 constraints.maxWidth)
328 .clamp(0.0, 1.0);
329 double value =
330 1.0 -
331 (localOffset.dy / constraints.maxHeight).clamp(
332 0.0,
333 1.0,
334 );
335
336 _handleHsvChange(
337 _currentHsv.withSaturation(saturation).withValue(value),
338 );
339 },
340 onPanEnd: (_) => _saveToRecent(),
341 child: Stack(
342 children: [
343 // Base Hue Color
344 Container(
345 color:
346 HSVColor.fromAHSV(
347 1.0,
348 _currentHsv.hue,
349 1.0,
350 1.0,
351 ).toColor(),
352 ),
353 // Gradient: White -> Transparent (Saturation)
354 Container(
355 decoration: const BoxDecoration(
356 gradient: LinearGradient(
357 colors: [Colors.white, Colors.transparent],
358 begin: Alignment.centerLeft,
359 end: Alignment.centerRight,
360 ),
361 ),
362 ),
363 // Gradient: Transparent -> Black (Value/Brightness)
364 Container(
365 decoration: const BoxDecoration(
366 gradient: LinearGradient(
367 colors: [Colors.transparent, Colors.black],
368 begin: Alignment.topCenter,
369 end: Alignment.bottomCenter,
370 ),
371 ),
372 ),
373 // Selector Circle
374 Positioned(
375 left:
376 _currentHsv.saturation * constraints.maxWidth -
377 10,
378 top:
379 (1 - _currentHsv.value) * constraints.maxHeight -
380 10,
381 child: Container(
382 width: 20,
383 height: 20,
384 decoration: BoxDecoration(
385 color: _currentColor,
386 shape: BoxShape.circle,
387 border: Border.all(color: Colors.white, width: 2),
388 boxShadow: const [
389 BoxShadow(color: Colors.black26, blurRadius: 4),
390 ],
391 ),
392 ),
393 ),
394 ],
395 ),
396 );
397 },
398 ),
399 ),
400 ),
401 const SizedBox(height: 24),
402 // Hue Slider
403 SizedBox(
404 height: 40,
405 child: Stack(
406 alignment: Alignment.center,
407 children: [
408 // Rainbow Background
409 Container(
410 height: 12,
411 decoration: BoxDecoration(
412 borderRadius: BorderRadius.circular(6),
413 gradient: const LinearGradient(
414 colors: [
415 Color(0xFFFF0000),
416 Color(0xFFFFFF00),
417 Color(0xFF00FF00),
418 Color(0xFF00FFFF),
419 Color(0xFF0000FF),
420 Color(0xFFFF00FF),
421 Color(0xFFFF0000),
422 ],
423 ),
424 ),
425 ),
426 SliderTheme(
427 data: SliderThemeData(
428 trackHeight: 12,
429 thumbShape: const RoundSliderThumbShape(
430 enabledThumbRadius: 14,
431 elevation: 4,
432 ),
433 overlayColor: Colors.transparent,
434 thumbColor: Colors.white,
435 activeTrackColor: Colors.transparent,
436 inactiveTrackColor: Colors.transparent,
437 ),
438 child: Slider(
439 value: _currentHsv.hue,
440 min: 0.0,
441 max: 360.0,
442 onChanged: (newHue) {
443 _handleHsvChange(_currentHsv.withHue(newHue));
444 },
445 onChangeEnd: (_) => _saveToRecent(),
446 ),
447 ),
448 ],
449 ),
450 ),
451 const SizedBox(height: 10),
452 ],
453 ),
454 );
455 }
456
457 // Tab 3: Sliders
458 Widget _buildSlidersTab() {
459 return Padding(
460 padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
461 child: SingleChildScrollView(
462 child: Column(
463 children: [
464 _buildSingleRGBSlider("Red", Colors.red, _currentColor.red, (v) {
465 _handleColorChange(_currentColor.withRed(v.toInt()));
466 }),
467 const SizedBox(height: 24),
468 _buildSingleRGBSlider("Green", Colors.green, _currentColor.green, (
469 v,
470 ) {
471 _handleColorChange(_currentColor.withGreen(v.toInt()));
472 }),
473 const SizedBox(height: 24),
474 _buildSingleRGBSlider("Blue", Colors.blue, _currentColor.blue, (v) {
475 _handleColorChange(_currentColor.withBlue(v.toInt()));
476 }),
477 ],
478 ),
479 ),
480 );
481 }
482
483 Widget _buildSingleRGBSlider(
484 String label,
485 Color activeColor,
486 int value,
487 ValueChanged<double> onChanged,
488 ) {
489 return Row(
490 children: [
491 Expanded(
492 child: SizedBox(
493 height: 30,
494 child: SliderTheme(
495 data: SliderThemeData(
496 trackHeight: 6,
497 activeTrackColor: activeColor,
498 inactiveTrackColor: activeColor.withOpacity(0.15),
499 thumbColor: Colors.white,
500 thumbShape: const RoundSliderThumbShape(
501 enabledThumbRadius: 14,
502 elevation: 4,
503 ),
504 overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
505 trackShape: const RoundedRectSliderTrackShape(),
506 ),
507 child: Slider(
508 value: value.toDouble(),
509 min: 0,
510 max: 255,
511 onChanged: onChanged,
512 onChangeEnd: (_) => _saveToRecent(),
513 ),
514 ),
515 ),
516 ),
517 const SizedBox(width: 12),
518 Container(
519 width: 50,
520 height: 36,
521 decoration: BoxDecoration(
522 border: Border.all(color: Colors.grey.shade300),
523 borderRadius: BorderRadius.circular(8),
524 color: Colors.white,
525 ),
526 alignment: Alignment.center,
527 child: Text(
528 value.toString(),
529 style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
530 ),
531 ),
532 ],
533 );
534 }
535
536 // Footer
537 Widget _buildSharedFooter() {
538 return SafeArea(
539 top: false,
540 child: Container(
541 padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
542 decoration: BoxDecoration(
543 color: Colors.white,
544 border: Border(top: BorderSide(color: Colors.grey.shade100)),
545 ),
546 child: Column(
547 mainAxisSize: MainAxisSize.min,
548 crossAxisAlignment: CrossAxisAlignment.start,
549 children: [
550 const Text(
551 "Recently used",
552 style: TextStyle(
553 fontSize: 12,
554 fontWeight: FontWeight.w600,
555 color: Colors.grey,
556 ),
557 ),
558 const SizedBox(height: 10),
559 SingleChildScrollView(
560 scrollDirection: Axis.horizontal,
561 child: Row(
562 children:
563 widget.recentColors
564 .map((c) => _buildColorCircle(c))
565 .toList(),
566 ),
567 ),
568 const SizedBox(height: 16),
569 Row(
570 mainAxisAlignment: MainAxisAlignment.spaceBetween,
571 children: [
572 const Text(
573 "Brand Palette",
574 style: TextStyle(
575 fontSize: 12,
576 fontWeight: FontWeight.w600,
577 color: Colors.grey,
578 ),
579 ),
580 Text(
581 "Edit",
582 style: TextStyle(
583 fontSize: 12,
584 color: Colors.blue[700],
585 fontWeight: FontWeight.bold,
586 ),
587 ),
588 ],
589 ),
590 const SizedBox(height: 10),
591 SingleChildScrollView(
592 scrollDirection: Axis.horizontal,
593 child: Row(
594 children: [
595 _buildAddButton(),
596 const SizedBox(width: 12),
597 ..._brandPalette.map((c) => _buildColorCircle(c)).toList(),
598 ],
599 ),
600 ),
601 ],
602 ),
603 ),
604 );
605 }
606
607 Widget _buildColorCircle(Color color) {
608 return GestureDetector(
609 onTap: () {
610 _handleColorChange(color);
611 _saveToRecent();
612 },
613 child: Container(
614 width: 36,
615 height: 36,
616 margin: const EdgeInsets.only(right: 12),
617 decoration: BoxDecoration(
618 color: color,
619 shape: BoxShape.circle,
620 border: Border.all(color: Colors.grey.shade200, width: 1),
621 boxShadow: [
622 BoxShadow(
623 color: Colors.black.withOpacity(0.05),
624 blurRadius: 4,
625 offset: const Offset(0, 2),
626 ),
627 ],
628 ),
629 ),
630 );
631 }
632
633 Widget _buildAddButton() {
634 return Container(
635 width: 36,
636 height: 36,
637 decoration: BoxDecoration(
638 color: Colors.grey[100],
639 shape: BoxShape.circle,
640 border: Border.all(color: Colors.grey.shade300),
641 ),
642 child: const Icon(Icons.add, size: 20, color: Colors.black54),
643 );
644 }
645}