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 | |
30 | namespace app { |
31 | |
32 | using namespace app::skin; |
33 | using namespace gfx; |
34 | using namespace ui; |
35 | |
36 | static 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 | |
51 | ColorWheel::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 | |
72 | const 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"( |
84 | const half PI = 3.1415; |
85 | |
86 | half 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 | |
93 | half4 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 | |
146 | const 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"( |
154 | half4 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 | |
163 | void 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 | |
175 | app::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 | |
278 | app::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 | |
288 | void 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 | |
344 | void 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 | |
354 | void 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 | |
407 | int 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 | |
418 | void 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 | |
429 | void ColorWheel::setColorModel(ColorModel colorModel) |
430 | { |
431 | m_colorModel = colorModel; |
432 | Preferences::instance().colorBar.wheelModel((int)m_colorModel); |
433 | |
434 | invalidate(); |
435 | } |
436 | |
437 | void ColorWheel::setHarmony(Harmony harmony) |
438 | { |
439 | m_harmony = harmony; |
440 | Preferences::instance().colorBar.harmony((int)m_harmony); |
441 | |
442 | invalidate(); |
443 | } |
444 | |
445 | int ColorWheel::getHarmonies() const |
446 | { |
447 | int i = std::clamp((int)m_harmony, 0, (int)Harmony::LAST); |
448 | return harmonies[i].n; |
449 | } |
450 | |
451 | app::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 | |
462 | void 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 | |
475 | void ColorWheel::onOptions() |
476 | { |
477 | 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 | |
529 | float 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 | |