1 //  SuperTux
2 //  Copyright (C) 2021
3 //
4 //  This program is free software: you can redistribute it and/or modify
5 //  it under the terms of the GNU General Public License as published by
6 //  the Free Software Foundation, either version 3 of the License, or
7 //  (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 
17 #include "gui/item_colorchannel.hpp"
18 
19 #include <vector>
20 
21 #include "math/util.hpp"
22 #include "video/drawing_context.hpp"
23 #include "video/video_system.hpp"
24 #include "video/viewport.hpp"
25 
26 
27 // This value affects the gamut clipping in the hue selection.
28 // A bigger value means more preserving of chroma instead of lightness.
29 constexpr float HUE_COLORFULNESS = 0.5f;
30 
ItemColorChannelOKLab(Color * col,int channel,Menu * menu)31 ItemColorChannelOKLab::ItemColorChannelOKLab(Color* col, int channel,
32     Menu* menu) :
33   MenuItem(""),
34   m_color(col),
35   m_col_prev(0, 0, 0),
36   m_channel(ChannelType::CHANNEL_L),
37   m_menu(menu),
38   m_mousedown(false)
39 {
40   if (channel == 1)
41     m_channel = ChannelType::CHANNEL_L;
42   else if (channel == 2)
43     m_channel = ChannelType::CHANNEL_C;
44   else
45     m_channel = ChannelType::CHANNEL_H;
46 }
47 
48 void
draw(DrawingContext & context,const Vector & pos,int menu_width,bool active)49 ItemColorChannelOKLab::draw(DrawingContext& context, const Vector& pos,
50   int menu_width, bool active)
51 {
52   const float lw = static_cast<float>(menu_width - 32);
53   ColorOKLCh col_oklch(0, 0, 0);
54   if (active) {
55     col_oklch = m_col_prev;
56   } else {
57     col_oklch = ColorOKLCh(*m_color);
58   }
59 
60   // Draw all possible colour values for the given component
61   float chroma_max_any_l = 1.0f;
62   if (m_channel == ChannelType::CHANNEL_C)
63     chroma_max_any_l = col_oklch.get_maximum_chroma_any_l();
64   constexpr int NUM_RECTS = 128;
65   std::vector<Color> colors(NUM_RECTS+1);
66   for (int i = 0; i < NUM_RECTS+1; ++i) {
67     ColorOKLCh col_oklch_current = col_oklch;
68     float x = static_cast<float>(i) / NUM_RECTS;
69     if (m_channel == ChannelType::CHANNEL_L) {
70       col_oklch_current.L = x;
71     } else if (m_channel == ChannelType::CHANNEL_C) {
72       col_oklch_current.C = x * chroma_max_any_l;
73       col_oklch_current.clip_lightness();
74     } else {
75       col_oklch_current.h = (2.0f * x - 1.0f) * math::PI;
76       col_oklch_current.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS);
77     }
78     colors[i] = col_oklch_current.to_srgb();
79   }
80   for (int i = 0; i < NUM_RECTS; ++i) {
81     float x1 = 16 + static_cast<float>(i) * lw / NUM_RECTS;
82     float x2 = x1 + lw / NUM_RECTS;
83     context.color().draw_gradient(colors[i], colors[i+1], LAYER_GUI-1,
84       GradientDirection::HORIZONTAL,
85       Rectf(pos + Vector(x1, -10), pos + Vector(x2, 10)));
86   }
87 
88   // Draw a marker for the current colour
89   float x_marker;
90   if (m_channel == ChannelType::CHANNEL_L) {
91     x_marker = col_oklch.L;
92   } else if (m_channel == ChannelType::CHANNEL_C) {
93     x_marker = chroma_max_any_l > 0.0f ? col_oklch.C / chroma_max_any_l : 0.0f;
94   } else {
95     x_marker = 0.5f * col_oklch.h / math::PI + 0.5f;
96   }
97   x_marker = pos.x + 16 + x_marker * lw;
98   context.color().draw_triangle(Vector(x_marker - 3, pos.y - 11),
99     Vector(x_marker + 3, pos.y - 11), Vector(x_marker, pos.y - 4),
100     Color::WHITE, LAYER_GUI-1);
101   context.color().draw_triangle(Vector(x_marker, pos.y + 4),
102     Vector(x_marker - 3, pos.y + 11), Vector(x_marker + 3, pos.y + 11),
103     Color::BLACK, LAYER_GUI-1);
104 
105   if (m_channel == ChannelType::CHANNEL_C && chroma_max_any_l > 0.0f) {
106     // Draw a marker where the lightness clipping starts
107     x_marker = col_oklch.get_maximum_chroma() / chroma_max_any_l;
108     x_marker = pos.x + 16 + x_marker * lw;
109     context.color().draw_triangle(Vector(x_marker - 2, pos.y - 11),
110       Vector(x_marker + 2, pos.y - 11), Vector(x_marker, pos.y),
111       Color(0.73f, 0.73f, 0.73f), LAYER_GUI-1);
112   }
113 }
114 
115 void
process_action(const MenuAction & action)116 ItemColorChannelOKLab::process_action(const MenuAction& action)
117 {
118   if (action == MenuAction::SELECT) {
119     m_col_prev = ColorOKLCh(*m_color);
120     return;
121   }
122   float increment;
123   if (action == MenuAction::LEFT)
124     increment = -0.1f;
125   else if (action == MenuAction::RIGHT)
126     increment = 0.1f;
127   else
128     return;
129 
130   ColorOKLCh col_oklch = m_col_prev;
131   ColorOKLCh col_oklch_clipped(0, 0, 0);
132   if (m_channel == ChannelType::CHANNEL_L) {
133     col_oklch.L = math::clamp(col_oklch.L + increment, 0.0f, 1.0f);
134     col_oklch_clipped = col_oklch;
135   } else if (m_channel == ChannelType::CHANNEL_C) {
136     float chroma_max = col_oklch.get_maximum_chroma_any_l();
137     increment *= chroma_max;
138     col_oklch.C = math::clamp(col_oklch.C + increment, 0.0f, chroma_max);
139     col_oklch_clipped = col_oklch;
140     col_oklch_clipped.clip_lightness();
141   } else {
142     increment *= 3.0f;
143     col_oklch.h = fmodf(col_oklch.h + increment + 3.0f * math::PI,
144       2.0f * math::PI) - math::PI;
145     col_oklch_clipped = col_oklch;
146     col_oklch_clipped.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS);
147   }
148   set_color(col_oklch_clipped, col_oklch);
149 }
150 
151 void
event(const SDL_Event & ev)152 ItemColorChannelOKLab::event(const SDL_Event& ev)
153 {
154   // Determine the new colour with the mouse position if either the mouse
155   // is clicked once or clicked and held down
156   bool is_mouseclick = ev.type == SDL_MOUSEBUTTONDOWN
157     && ev.button.button == SDL_BUTTON_LEFT;
158   bool is_hold_mousemove = ev.type == SDL_MOUSEMOTION
159     && (ev.motion.state & SDL_BUTTON_LMASK);
160   if (is_mouseclick) {
161     m_mousedown = true;
162   } else if (!is_hold_mousemove || !m_mousedown) {
163     m_mousedown = false;
164     return;
165   }
166 
167   Vector mouse_pos = VideoSystem::current()->get_viewport().to_logical(
168     ev.motion.x, ev.motion.y);
169 
170   // Calculate the menu item positions as passed in the draw method
171   Vector menu_centre = m_menu->get_center_pos();
172   const float menu_width = m_menu->get_width();
173   const float menu_height = m_menu->get_height();
174   Vector pos(
175     menu_centre.x - menu_width / 2.0f,
176     menu_centre.y
177     + 24.0f * static_cast<float>(m_menu->get_active_item_id())
178     - menu_height / 2.0f + 12.0f
179   );
180 
181   // Calculate the relative horizontal position
182   float x1 = pos.x + 16.0f;
183   float x2 = pos.x + menu_width - 16.0f;
184   float x = (mouse_pos.x - x1) / (x2 - x1);
185   if (m_channel != ChannelType::CHANNEL_H) {
186     x = math::clamp(x, 0.0f, 1.0f);
187   } else {
188     // The hue is periodic
189     x = fmodf(x + 3.0f, 1.0f);
190   }
191 
192   // Ignore distant mouse presses
193   if (x < -0.5f || x > 1.5f || mouse_pos.y > pos.y + menu_height / 2.0f
194       || mouse_pos.y < pos.y - menu_height / 2.0f)
195     return;
196 
197   ColorOKLCh col_oklch = m_col_prev;
198   ColorOKLCh col_oklch_clipped(0, 0, 0);
199   if (m_channel == ChannelType::CHANNEL_L) {
200     col_oklch.L = x;
201     col_oklch_clipped = col_oklch;
202   } else if (m_channel == ChannelType::CHANNEL_C) {
203     float chroma_max_any_l = col_oklch.get_maximum_chroma_any_l();
204     col_oklch.C = x * chroma_max_any_l;
205     col_oklch_clipped = col_oklch;
206     col_oklch_clipped.clip_lightness();
207   } else {
208     col_oklch.h = (2.0f * x - 1.0f) * math::PI;
209     col_oklch_clipped = col_oklch;
210     col_oklch_clipped.clip_adaptive_L0_L_cusp(HUE_COLORFULNESS);
211   }
212   set_color(col_oklch_clipped, col_oklch);
213 }
214 
215 void
set_color(ColorOKLCh & col_oklch_clipped,ColorOKLCh & col_oklch_store)216 ItemColorChannelOKLab::set_color(ColorOKLCh& col_oklch_clipped,
217   ColorOKLCh& col_oklch_store)
218 {
219   // Save the current unclipped colour
220   m_col_prev = col_oklch_store;
221   // Convert the colour back to sRGB and clip if needed; preserve transparency
222   float alpha = m_color->alpha;
223   *m_color = col_oklch_clipped.to_srgb();
224   m_color->alpha = alpha;
225 }
226 
227 /* EOF */
228