1// Aseprite
2// Copyright (C) 2020-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/color_wheel.h"
13
14#include "app/color_utils.h"
15#include "app/i18n/strings.h"
16#include "app/pref/preferences.h"
17#include "app/ui/skin/skin_theme.h"
18#include "app/ui/status_bar.h"
19#include "app/util/shader_helpers.h"
20#include "base/pi.h"
21#include "os/surface.h"
22#include "ui/graphics.h"
23#include "ui/menu.h"
24#include "ui/message.h"
25#include "ui/paint_event.h"
26#include "ui/resize_event.h"
27#include "ui/size_hint_event.h"
28#include "ui/system.h"
29
30namespace app {
31
32using namespace app::skin;
33using namespace gfx;
34using namespace ui;
35
36static struct {
37 int n;
38 int hues[4];
39 int sats[4];
40} harmonies[] = {
41 { 1, { 0, 0, 0, 0 }, { 100, 0, 0, 0 } }, // NONE
42 { 2, { 0, 180, 0, 0 }, { 100, 100, 0, 0 } }, // COMPLEMENTARY
43 { 2, { 0, 0, 0, 0 }, { 100, 50, 0, 0 } }, // MONOCHROMATIC
44 { 3, { 0, 30, 330, 0 }, { 100, 100, 100, 0 } }, // ANALOGOUS
45 { 3, { 0, 150, 210, 0 }, { 100, 100, 100, 0 } }, // SPLIT
46 { 3, { 0, 120, 240, 0 }, { 100, 100, 100, 0 } }, // TRIADIC
47 { 4, { 0, 120, 180, 300 }, { 100, 100, 100, 100 } }, // TETRADIC
48 { 4, { 0, 90, 180, 270 }, { 100, 100, 100, 100 } }, // SQUARE
49};
50
51ColorWheel::ColorWheel()
52 : m_discrete(Preferences::instance().colorBar.discreteWheel())
53 , m_colorModel((ColorModel)Preferences::instance().colorBar.wheelModel())
54 , m_harmony((Harmony)Preferences::instance().colorBar.harmony())
55 , m_options("")
56 , m_harmonyPicked(false)
57{
58 m_options.Click.connect([this]{ onOptions(); });
59 addChild(&m_options);
60
61 InitTheme.connect(
62 [this]{
63 auto theme = skin::SkinTheme::get(this);
64 m_options.setStyle(theme->styles.colorWheelOptions());
65 m_bgColor = theme->colors.editorFace();
66 });
67 initTheme();
68}
69
70#if SK_ENABLE_SKSL
71
72const char* ColorWheel::getMainAreaShader()
73{
74 // TODO create one shader for each wheel mode (RGB, RYB, normal)
75 if (m_mainShader.empty()) {
76 m_mainShader += "uniform half3 iRes;"
77 "uniform half4 iHsv;"
78 "uniform half4 iBack;"
79 "uniform int iDiscrete;"
80 "uniform int iMode;";
81 m_mainShader += kRGB_to_HSV_sksl;
82 m_mainShader += kHSV_to_RGB_sksl;
83 m_mainShader += R"(
84const half PI = 3.1415;
85
86half rybhue_to_rgbhue(half h) {
87 if (h >= 0 && h < 120) return h / 2; // from red to yellow
88 else if (h < 180) return (h-60.0); // from yellow to green
89 else if (h < 240) return 120 + 2*(h-180); // from green to blue
90 else return h; // from blue to red (same hue)
91}
92
93half4 main(vec2 fragcoord) {
94 vec2 res = vec2(min(iRes.x, iRes.y), min(iRes.x, iRes.y));
95 vec2 d = (fragcoord.xy-iRes.xy/2) / res.xy;
96 half r = length(d);
97
98 if (r <= 0.5) {
99 half a = atan(-d.y, d.x);
100 half hue = (floor(180.0 * a / PI)
101 + 180 // To avoid [-180,0) range
102 + 180 + 30 // To locate green at 12 o'clock
103 );
104
105 hue = mod(hue, 360); // To leave hue in [0,360) range
106 if (iDiscrete != 0) {
107 hue += 15.0;
108 hue = floor(hue / 30.0);
109 hue *= 30.0;
110 }
111 if (iMode == 1) { // RYB color wheel
112 hue = rybhue_to_rgbhue(hue);
113 }
114 hue /= 360.0;
115
116 if (iMode == 2) { // Normal map mode
117 float di = 0.5 * r / 0.5;
118 half3 rgb = half3(0.5+di*cos(a), 0.5+di*sin(a), 1.0-di);
119 return half4(
120 clamp(rgb.x, 0, 1),
121 clamp(rgb.y, 0, 1),
122 clamp(rgb.z, 0.5, 1), 1);
123 }
124
125 half sat = r / 0.5;
126 if (iDiscrete != 0) {
127 sat *= 120.0;
128 sat = floor(sat / 20.0);
129 sat *= 20.0;
130 sat /= 100.0;
131 sat = clamp(sat, 0.0, 1.0);
132 }
133 return hsv_to_rgb(vec3(hue, sat, iHsv.w > 0 ? iHsv.z: 1.0)).rgb1;
134 }
135 else {
136 if (iMode == 2) // Normal map mode
137 return half4(0.5, 0.5, 1, 1);
138 return iBack;
139 }
140}
141)";
142 }
143 return m_mainShader.c_str();
144}
145
146const char* ColorWheel::getBottomBarShader()
147{
148 if (m_bottomShader.empty()) {
149 m_bottomShader += "uniform half3 iRes;"
150 "uniform half4 iHsv;";
151 m_bottomShader += kHSV_to_RGB_sksl;
152 // TODO should we display the hue bar with the current sat/value?
153 m_bottomShader += R"(
154half4 main(vec2 fragcoord) {
155 half v = (fragcoord.x / iRes.x);
156 return hsv_to_rgb(half3(iHsv.x, iHsv.y, v)).rgb1;
157}
158)";
159 }
160 return m_bottomShader.c_str();
161}
162
163void ColorWheel::setShaderParams(SkRuntimeShaderBuilder& builder, bool main)
164{
165 builder.uniform("iHsv") = appColorHsv_to_SkV4(m_color);
166 if (main) {
167 builder.uniform("iBack") = gfxColor_to_SkV4(m_bgColor);
168 builder.uniform("iDiscrete") = (m_discrete ? 1: 0);
169 builder.uniform("iMode") = int(m_colorModel);
170 }
171}
172
173#endif // SK_ENABLE_SKSL
174
175app::Color ColorWheel::getMainAreaColor(const int _u, const int umax,
176 const int _v, const int vmax)
177{
178 m_harmonyPicked = false;
179
180 int u = _u - umax/2;
181 int v = _v - vmax/2;
182
183 // Pick harmonies
184 if (m_color.getAlpha() > 0) {
185 const gfx::Point pos(_u, _v);
186 int n = getHarmonies();
187 int boxsize = std::min(umax/10, vmax/10);
188
189 for (int i=0; i<n; ++i) {
190 app::Color color = getColorInHarmony(i);
191
192 if (gfx::Rect(umax-(n-i)*boxsize,
193 vmax-boxsize,
194 boxsize, boxsize).contains(pos)) {
195 m_harmonyPicked = true;
196
197 color = app::Color::fromHsv(convertHueAngle(color.getHsvHue(), 1),
198 color.getHsvSaturation(),
199 color.getHsvValue(),
200 m_color.getAlpha());
201 return color;
202 }
203 }
204 }
205
206 double d = std::sqrt(u*u + v*v);
207
208 // When we click the main area we can limit the distance to the
209 // wheel radius to pick colors even outside the wheel radius.
210 if (hasCaptureInMainArea() && d > m_wheelRadius)
211 d = m_wheelRadius;
212
213 if (m_colorModel == ColorModel::NORMAL_MAP) {
214 double a = std::atan2(-v, u);
215 int di = int(128.0 * d / m_wheelRadius);
216
217 if (m_discrete) {
218 int ai = (int(180.0 * a / PI) + 360);
219 ai += 15;
220 ai /= 30;
221 ai *= 30;
222 a = PI * ai / 180.0;
223
224 di /= 32;
225 di *= 32;
226 }
227
228 int r = 128 + di*std::cos(a);
229 int g = 128 + di*std::sin(a);
230 int b = 255 - di;
231 if (d <= m_wheelRadius) {
232 return app::Color::fromRgb(
233 std::clamp(r, 0, 255),
234 std::clamp(g, 0, 255),
235 std::clamp(b, 128, 255));
236 }
237 else {
238 return app::Color::fromRgb(128, 128, 255);
239 }
240 }
241
242 // Pick from the wheel
243 if (d <= m_wheelRadius) {
244 double a = std::atan2(-v, u);
245
246 int hue = (int(180.0 * a / PI)
247 + 180 // To avoid [-180,0) range
248 + 180 + 30 // To locate green at 12 o'clock
249 );
250 if (m_discrete) {
251 hue += 15;
252 hue /= 30;
253 hue *= 30;
254 }
255 hue %= 360; // To leave hue in [0,360) range
256 hue = convertHueAngle(hue, 1);
257
258 int sat;
259 if (m_discrete) {
260 sat = int(120.0 * d / m_wheelRadius);
261 sat /= 20;
262 sat *= 20;
263 }
264 else {
265 sat = int(100.0 * d / m_wheelRadius);
266 }
267
268 return app::Color::fromHsv(
269 std::clamp(hue, 0, 360),
270 std::clamp(sat / 100.0, 0.0, 1.0),
271 (m_color.getType() != Color::MaskType ? m_color.getHsvValue(): 1.0),
272 getCurrentAlphaForNewColor());
273 }
274
275 return app::Color::fromMask();
276}
277
278app::Color ColorWheel::getBottomBarColor(const int u, const int umax)
279{
280 double val = double(u) / double(umax);
281 return app::Color::fromHsv(
282 m_color.getHsvHue(),
283 m_color.getHsvSaturation(),
284 std::clamp(val, 0.0, 1.0),
285 getCurrentAlphaForNewColor());
286}
287
288void ColorWheel::onPaintMainArea(ui::Graphics* g, const gfx::Rect& rc)
289{
290 bool oldHarmonyPicked = m_harmonyPicked;
291
292 double r = std::max(1.0, std::min(rc.w, rc.h) / 2.0);
293 m_wheelRadius = r-0.1;
294 m_wheelBounds = gfx::Rect(rc.x+rc.w/2-r,
295 rc.y+rc.h/2-r,
296 r*2, r*2);
297
298 if (m_color.getAlpha() > 0) {
299 if (m_colorModel == ColorModel::NORMAL_MAP) {
300 double angle = std::atan2(m_color.getGreen()-128,
301 m_color.getRed()-128);
302 double dist = (255-m_color.getBlue()) / 128.0;
303 dist = std::clamp(dist, 0.0, 1.0);
304
305 gfx::Point pos =
306 m_wheelBounds.center() +
307 gfx::Point(int(+std::cos(angle)*double(m_wheelRadius)*dist),
308 int(-std::sin(angle)*double(m_wheelRadius)*dist));
309 paintColorIndicator(g, pos, true);
310 }
311 else {
312 int n = getHarmonies();
313 int boxsize = std::min(rc.w/10, rc.h/10);
314
315 for (int i=0; i<n; ++i) {
316 app::Color color = getColorInHarmony(i);
317 double angle = color.getHsvHue()-30.0;
318 double dist = color.getHsvSaturation();
319
320 color = app::Color::fromHsv(convertHueAngle(color.getHsvHue(), 1),
321 color.getHsvSaturation(),
322 color.getHsvValue());
323
324 gfx::Point pos =
325 m_wheelBounds.center() +
326 gfx::Point(int(+std::cos(PI*angle/180.0)*double(m_wheelRadius)*dist),
327 int(-std::sin(PI*angle/180.0)*double(m_wheelRadius)*dist));
328
329 paintColorIndicator(g, pos, color.getHsvValue() < 0.5);
330
331 g->fillRect(gfx::rgba(color.getRed(),
332 color.getGreen(),
333 color.getBlue(), 255),
334 gfx::Rect(rc.x+rc.w-(n-i)*boxsize,
335 rc.y+rc.h-boxsize,
336 boxsize, boxsize));
337 }
338 }
339 }
340
341 m_harmonyPicked = oldHarmonyPicked;
342}
343
344void ColorWheel::onPaintBottomBar(ui::Graphics* g, const gfx::Rect& rc)
345{
346 if (m_color.getType() != app::Color::MaskType) {
347 double val = m_color.getHsvValue();
348 gfx::Point pos(rc.x + int(double(rc.w) * val),
349 rc.y + rc.h/2);
350 paintColorIndicator(g, pos, val < 0.5);
351 }
352}
353
354void ColorWheel::onPaintSurfaceInBgThread(os::Surface* s,
355 const gfx::Rect& main,
356 const gfx::Rect& bottom,
357 const gfx::Rect& alpha,
358 bool& stop)
359{
360 if (m_paintFlags & MainAreaFlag) {
361 int umax = std::max(1, main.w-1);
362 int vmax = std::max(1, main.h-1);
363
364 for (int y=0; y<main.h && !stop; ++y) {
365 for (int x=0; x<main.w && !stop; ++x) {
366 app::Color appColor =
367 getMainAreaColor(x, umax,
368 y, vmax);
369
370 gfx::Color color;
371 if (appColor.getType() != app::Color::MaskType) {
372 appColor.setAlpha(255);
373 color = color_utils::color_for_ui(appColor);
374 }
375 else {
376 color = m_bgColor;
377 }
378
379 s->putPixel(color, main.x+x, main.y+y);
380 }
381 }
382 if (stop)
383 return;
384 m_paintFlags ^= MainAreaFlag;
385 }
386
387 if (m_paintFlags & BottomBarFlag) {
388 double hue = m_color.getHsvHue();
389 double sat = m_color.getHsvSaturation();
390 os::Paint paint;
391 for (int x=0; x<bottom.w && !stop; ++x) {
392 paint.color(
393 color_utils::color_for_ui(
394 app::Color::fromHsv(hue, sat, double(x) / double(bottom.w))));
395
396 s->drawRect(gfx::Rect(bottom.x+x, bottom.y, 1, bottom.h), paint);
397 }
398 if (stop)
399 return;
400 m_paintFlags ^= BottomBarFlag;
401 }
402
403 // Paint alpha bar
404 ColorSelector::onPaintSurfaceInBgThread(s, main, bottom, alpha, stop);
405}
406
407int ColorWheel::onNeedsSurfaceRepaint(const app::Color& newColor)
408{
409 return
410 // Only if the saturation changes we have to redraw the main surface.
411 (m_colorModel != ColorModel::NORMAL_MAP &&
412 cs_double_diff(m_color.getHsvValue(), newColor.getHsvValue()) ? MainAreaFlag: 0) |
413 (cs_double_diff(m_color.getHsvHue(), newColor.getHsvHue()) ||
414 cs_double_diff(m_color.getHsvSaturation(), newColor.getHsvSaturation()) ? BottomBarFlag: 0) |
415 ColorSelector::onNeedsSurfaceRepaint(newColor);
416}
417
418void ColorWheel::setDiscrete(bool state)
419{
420 if (m_discrete != state)
421 m_paintFlags = AllAreasFlag;
422
423 m_discrete = state;
424 Preferences::instance().colorBar.discreteWheel(m_discrete);
425
426 invalidate();
427}
428
429void ColorWheel::setColorModel(ColorModel colorModel)
430{
431 m_colorModel = colorModel;
432 Preferences::instance().colorBar.wheelModel((int)m_colorModel);
433
434 invalidate();
435}
436
437void ColorWheel::setHarmony(Harmony harmony)
438{
439 m_harmony = harmony;
440 Preferences::instance().colorBar.harmony((int)m_harmony);
441
442 invalidate();
443}
444
445int ColorWheel::getHarmonies() const
446{
447 int i = std::clamp((int)m_harmony, 0, (int)Harmony::LAST);
448 return harmonies[i].n;
449}
450
451app::Color ColorWheel::getColorInHarmony(int j) const
452{
453 int i = std::clamp((int)m_harmony, 0, (int)Harmony::LAST);
454 j = std::clamp(j, 0, harmonies[i].n-1);
455 double hue = convertHueAngle(m_color.getHsvHue(), -1) + harmonies[i].hues[j];
456 double sat = m_color.getHsvSaturation() * harmonies[i].sats[j] / 100.0;
457 return app::Color::fromHsv(std::fmod(hue, 360),
458 std::clamp(sat, 0.0, 1.0),
459 m_color.getHsvValue());
460}
461
462void ColorWheel::onResize(ui::ResizeEvent& ev)
463{
464 ColorSelector::onResize(ev);
465
466 gfx::Rect rc = clientChildrenBounds();
467 gfx::Size prefSize = m_options.sizeHint();
468 rc = childrenBounds();
469 rc.x += rc.w-prefSize.w;
470 rc.w = prefSize.w;
471 rc.h = prefSize.h;
472 m_options.setBounds(rc);
473}
474
475void ColorWheel::onOptions()
476{
477 Menu menu;
478 MenuItem discrete(Strings::color_wheel_discrete());
479 MenuItem none(Strings::color_wheel_no_harmonies());
480 MenuItem complementary(Strings::color_wheel_complementary());
481 MenuItem monochromatic(Strings::color_wheel_monochromatic());
482 MenuItem analogous(Strings::color_wheel_analogous());
483 MenuItem split(Strings::color_wheel_split_complementary());
484 MenuItem triadic(Strings::color_wheel_triadic());
485 MenuItem tetradic(Strings::color_wheel_tetradic());
486 MenuItem square(Strings::color_wheel_square());
487 menu.addChild(&discrete);
488 if (m_colorModel != ColorModel::NORMAL_MAP) {
489 menu.addChild(new MenuSeparator);
490 menu.addChild(&none);
491 menu.addChild(&complementary);
492 menu.addChild(&monochromatic);
493 menu.addChild(&analogous);
494 menu.addChild(&split);
495 menu.addChild(&triadic);
496 menu.addChild(&tetradic);
497 menu.addChild(&square);
498 }
499
500 if (isDiscrete())
501 discrete.setSelected(true);
502 discrete.Click.connect([this]{ setDiscrete(!isDiscrete()); });
503
504 if (m_colorModel != ColorModel::NORMAL_MAP) {
505 switch (m_harmony) {
506 case Harmony::NONE: none.setSelected(true); break;
507 case Harmony::COMPLEMENTARY: complementary.setSelected(true); break;
508 case Harmony::MONOCHROMATIC: monochromatic.setSelected(true); break;
509 case Harmony::ANALOGOUS: analogous.setSelected(true); break;
510 case Harmony::SPLIT: split.setSelected(true); break;
511 case Harmony::TRIADIC: triadic.setSelected(true); break;
512 case Harmony::TETRADIC: tetradic.setSelected(true); break;
513 case Harmony::SQUARE: square.setSelected(true); break;
514 }
515 none.Click.connect([this]{ setHarmony(Harmony::NONE); });
516 complementary.Click.connect([this]{ setHarmony(Harmony::COMPLEMENTARY); });
517 monochromatic.Click.connect([this]{ setHarmony(Harmony::MONOCHROMATIC); });
518 analogous.Click.connect([this]{ setHarmony(Harmony::ANALOGOUS); });
519 split.Click.connect([this]{ setHarmony(Harmony::SPLIT); });
520 triadic.Click.connect([this]{ setHarmony(Harmony::TRIADIC); });
521 tetradic.Click.connect([this]{ setHarmony(Harmony::TETRADIC); });
522 square.Click.connect([this]{ setHarmony(Harmony::SQUARE); });
523 }
524
525 gfx::Rect rc = m_options.bounds();
526 menu.showPopup(gfx::Point(rc.x2(), rc.y), display());
527}
528
529float ColorWheel::convertHueAngle(float h, int dir) const
530{
531 if (m_colorModel == ColorModel::RYB) {
532 if (dir == 1) {
533 // rybhue_to_rgbhue() maps:
534 // [0,120) -> [0,60)
535 // [120,180) -> [60,120)
536 // [180,240) -> [120,240)
537 // [240,360] -> [240,360]
538 if (h >= 0 && h < 120) return h / 2; // from red to yellow
539 else if (h < 180) return (h-60); // from yellow to green
540 else if (h < 240) return 120 + 2*(h-180); // from green to blue
541 else return h; // from blue to red (same hue)
542 }
543 else {
544 // rgbhue_to_rybhue()
545 // [0,60) -> [0,120)
546 // [60,120) -> [120,180)
547 // [120,240) -> [180,240)
548 // [240,360] -> [240,360]
549 if (h >= 0 && h < 60) return 2 * h; // from red to yellow
550 else if (h < 120) return 60 + h; // from yellow to green
551 else if (h < 240) return 180 + (h-120)/2; // from green to blue
552 else return h; // from blue to red (same hue)
553 }
554 }
555 return h;
556}
557
558} // namespace app
559