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