1 | /* |
2 | src/colorwheel.cpp -- fancy analog widget to select a color value |
3 | |
4 | This widget was contributed by Dmitriy Morozov. |
5 | |
6 | NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. |
7 | The widget drawing code is based on the NanoVG demo application |
8 | by Mikko Mononen. |
9 | |
10 | All rights reserved. Use of this source code is governed by a |
11 | BSD-style license that can be found in the LICENSE.txt file. |
12 | */ |
13 | |
14 | #include <nanogui/colorwheel.h> |
15 | #include <nanogui/theme.h> |
16 | #include <nanogui/opengl.h> |
17 | #include <nanogui/serializer/core.h> |
18 | #include <Eigen/QR> |
19 | #include <Eigen/Geometry> |
20 | |
21 | NAMESPACE_BEGIN(nanogui) |
22 | |
23 | ColorWheel::ColorWheel(Widget *parent, const Color& rgb) |
24 | : Widget(parent), mDragRegion(None) { |
25 | setColor(rgb); |
26 | } |
27 | |
28 | Vector2i ColorWheel::preferredSize(NVGcontext *) const { |
29 | return { 100, 100. }; |
30 | } |
31 | |
32 | void ColorWheel::draw(NVGcontext *ctx) { |
33 | Widget::draw(ctx); |
34 | |
35 | if (!mVisible) |
36 | return; |
37 | |
38 | float x = mPos.x(), |
39 | y = mPos.y(), |
40 | w = mSize.x(), |
41 | h = mSize.y(); |
42 | |
43 | NVGcontext* vg = ctx; |
44 | |
45 | int i; |
46 | float r0, r1, ax,ay, bx,by, cx,cy, aeps, r; |
47 | float hue = mHue; |
48 | NVGpaint paint; |
49 | |
50 | nvgSave(vg); |
51 | |
52 | cx = x + w*0.5f; |
53 | cy = y + h*0.5f; |
54 | r1 = (w < h ? w : h) * 0.5f - 5.0f; |
55 | r0 = r1 * .75f; |
56 | |
57 | aeps = 0.5f / r1; // half a pixel arc length in radians (2pi cancels out). |
58 | |
59 | for (i = 0; i < 6; i++) { |
60 | float a0 = (float)i / 6.0f * NVG_PI * 2.0f - aeps; |
61 | float a1 = (float)(i+1.0f) / 6.0f * NVG_PI * 2.0f + aeps; |
62 | nvgBeginPath(vg); |
63 | nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW); |
64 | nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW); |
65 | nvgClosePath(vg); |
66 | ax = cx + cosf(a0) * (r0+r1)*0.5f; |
67 | ay = cy + sinf(a0) * (r0+r1)*0.5f; |
68 | bx = cx + cosf(a1) * (r0+r1)*0.5f; |
69 | by = cy + sinf(a1) * (r0+r1)*0.5f; |
70 | paint = nvgLinearGradient(vg, ax, ay, bx, by, |
71 | nvgHSLA(a0 / (NVG_PI * 2), 1.0f, 0.55f, 255), |
72 | nvgHSLA(a1 / (NVG_PI * 2), 1.0f, 0.55f, 255)); |
73 | nvgFillPaint(vg, paint); |
74 | nvgFill(vg); |
75 | } |
76 | |
77 | nvgBeginPath(vg); |
78 | nvgCircle(vg, cx,cy, r0-0.5f); |
79 | nvgCircle(vg, cx,cy, r1+0.5f); |
80 | nvgStrokeColor(vg, nvgRGBA(0,0,0,64)); |
81 | nvgStrokeWidth(vg, 1.0f); |
82 | nvgStroke(vg); |
83 | |
84 | // Selector |
85 | nvgSave(vg); |
86 | nvgTranslate(vg, cx,cy); |
87 | nvgRotate(vg, hue*NVG_PI*2); |
88 | |
89 | // Marker on |
90 | float u = std::max(r1/50, 1.5f); |
91 | u = std::min(u, 4.f); |
92 | nvgStrokeWidth(vg, u); |
93 | nvgBeginPath(vg); |
94 | nvgRect(vg, r0-1,-2*u,r1-r0+2,4*u); |
95 | nvgStrokeColor(vg, nvgRGBA(255,255,255,192)); |
96 | nvgStroke(vg); |
97 | |
98 | paint = nvgBoxGradient(vg, r0-3,-5,r1-r0+6,10, 2,4, nvgRGBA(0,0,0,128), nvgRGBA(0,0,0,0)); |
99 | nvgBeginPath(vg); |
100 | nvgRect(vg, r0-2-10,-4-10,r1-r0+4+20,8+20); |
101 | nvgRect(vg, r0-2,-4,r1-r0+4,8); |
102 | nvgPathWinding(vg, NVG_HOLE); |
103 | nvgFillPaint(vg, paint); |
104 | nvgFill(vg); |
105 | |
106 | // Center triangle |
107 | r = r0 - 6; |
108 | ax = cosf(120.0f/180.0f*NVG_PI) * r; |
109 | ay = sinf(120.0f/180.0f*NVG_PI) * r; |
110 | bx = cosf(-120.0f/180.0f*NVG_PI) * r; |
111 | by = sinf(-120.0f/180.0f*NVG_PI) * r; |
112 | nvgBeginPath(vg); |
113 | nvgMoveTo(vg, r,0); |
114 | nvgLineTo(vg, ax, ay); |
115 | nvgLineTo(vg, bx, by); |
116 | nvgClosePath(vg); |
117 | paint = nvgLinearGradient(vg, r, 0, ax, ay, nvgHSLA(hue, 1.0f, 0.5f, 255), |
118 | nvgRGBA(255, 255, 255, 255)); |
119 | nvgFillPaint(vg, paint); |
120 | nvgFill(vg); |
121 | paint = nvgLinearGradient(vg, (r + ax) * 0.5f, (0 + ay) * 0.5f, bx, by, |
122 | nvgRGBA(0, 0, 0, 0), nvgRGBA(0, 0, 0, 255)); |
123 | nvgFillPaint(vg, paint); |
124 | nvgFill(vg); |
125 | nvgStrokeColor(vg, nvgRGBA(0, 0, 0, 64)); |
126 | nvgStroke(vg); |
127 | |
128 | // Select circle on triangle |
129 | float sx = r*(1 - mWhite - mBlack) + ax*mWhite + bx*mBlack; |
130 | float sy = ay*mWhite + by*mBlack; |
131 | |
132 | nvgStrokeWidth(vg, u); |
133 | nvgBeginPath(vg); |
134 | nvgCircle(vg, sx,sy,2*u); |
135 | nvgStrokeColor(vg, nvgRGBA(255,255,255,192)); |
136 | nvgStroke(vg); |
137 | |
138 | nvgRestore(vg); |
139 | |
140 | nvgRestore(vg); |
141 | } |
142 | |
143 | bool ColorWheel::mouseButtonEvent(const Vector2i &p, int button, bool down, |
144 | int modifiers) { |
145 | Widget::mouseButtonEvent(p, button, down, modifiers); |
146 | if (!mEnabled || button != GLFW_MOUSE_BUTTON_1) |
147 | return false; |
148 | |
149 | if (down) { |
150 | mDragRegion = adjustPosition(p); |
151 | return mDragRegion != None; |
152 | } else { |
153 | mDragRegion = None; |
154 | return true; |
155 | } |
156 | } |
157 | |
158 | bool ColorWheel::mouseDragEvent(const Vector2i &p, const Vector2i &, |
159 | int, int) { |
160 | return adjustPosition(p, mDragRegion) != None; |
161 | } |
162 | |
163 | ColorWheel::Region ColorWheel::adjustPosition(const Vector2i &p, Region consideredRegions) { |
164 | float x = p.x() - mPos.x(), |
165 | y = p.y() - mPos.y(), |
166 | w = mSize.x(), |
167 | h = mSize.y(); |
168 | |
169 | float cx = w*0.5f; |
170 | float cy = h*0.5f; |
171 | float r1 = (w < h ? w : h) * 0.5f - 5.0f; |
172 | float r0 = r1 * .75f; |
173 | |
174 | x -= cx; |
175 | y -= cy; |
176 | |
177 | float mr = std::sqrt(x*x + y*y); |
178 | |
179 | if ((consideredRegions & OuterCircle) && |
180 | ((mr >= r0 && mr <= r1) || (consideredRegions == OuterCircle))) { |
181 | if (!(consideredRegions & OuterCircle)) |
182 | return None; |
183 | mHue = std::atan(y / x); |
184 | if (x < 0) |
185 | mHue += NVG_PI; |
186 | mHue /= 2*NVG_PI; |
187 | |
188 | if (mCallback) |
189 | mCallback(color()); |
190 | |
191 | return OuterCircle; |
192 | } |
193 | |
194 | float r = r0 - 6; |
195 | |
196 | float ax = std::cos( 120.0f/180.0f*NVG_PI) * r; |
197 | float ay = std::sin( 120.0f/180.0f*NVG_PI) * r; |
198 | float bx = std::cos(-120.0f/180.0f*NVG_PI) * r; |
199 | float by = std::sin(-120.0f/180.0f*NVG_PI) * r; |
200 | |
201 | typedef Eigen::Matrix<float,2,2> Matrix2f; |
202 | |
203 | Eigen::Matrix<float, 2, 3> triangle; |
204 | triangle << ax,bx,r, |
205 | ay,by,0; |
206 | triangle = Eigen::Rotation2D<float>(mHue * 2 * NVG_PI).matrix() * triangle; |
207 | |
208 | Matrix2f T; |
209 | T << triangle(0,0) - triangle(0,2), triangle(0,1) - triangle(0,2), |
210 | triangle(1,0) - triangle(1,2), triangle(1,1) - triangle(1,2); |
211 | Vector2f pos { x - triangle(0,2), y - triangle(1,2) }; |
212 | |
213 | Vector2f bary = T.colPivHouseholderQr().solve(pos); |
214 | float l0 = bary[0], l1 = bary[1], l2 = 1 - l0 - l1; |
215 | bool triangleTest = l0 >= 0 && l0 <= 1.f && l1 >= 0.f && l1 <= 1.f && |
216 | l2 >= 0.f && l2 <= 1.f; |
217 | |
218 | if ((consideredRegions & InnerTriangle) && |
219 | (triangleTest || consideredRegions == InnerTriangle)) { |
220 | if (!(consideredRegions & InnerTriangle)) |
221 | return None; |
222 | l0 = std::min(std::max(0.f, l0), 1.f); |
223 | l1 = std::min(std::max(0.f, l1), 1.f); |
224 | l2 = std::min(std::max(0.f, l2), 1.f); |
225 | float sum = l0 + l1 + l2; |
226 | l0 /= sum; |
227 | l1 /= sum; |
228 | mWhite = l0; |
229 | mBlack = l1; |
230 | if (mCallback) |
231 | mCallback(color()); |
232 | return InnerTriangle; |
233 | } |
234 | |
235 | return None; |
236 | } |
237 | |
238 | Color ColorWheel::hue2rgb(float h) const { |
239 | float s = 1., v = 1.; |
240 | |
241 | if (h < 0) h += 1; |
242 | |
243 | int i = int(h * 6); |
244 | float f = h * 6 - i; |
245 | float p = v * (1 - s); |
246 | float q = v * (1 - f * s); |
247 | float t = v * (1 - (1 - f) * s); |
248 | |
249 | float r = 0, g = 0, b = 0; |
250 | switch (i % 6) { |
251 | case 0: r = v, g = t, b = p; break; |
252 | case 1: r = q, g = v, b = p; break; |
253 | case 2: r = p, g = v, b = t; break; |
254 | case 3: r = p, g = q, b = v; break; |
255 | case 4: r = t, g = p, b = v; break; |
256 | case 5: r = v, g = p, b = q; break; |
257 | } |
258 | |
259 | return { r, g, b, 1.f }; |
260 | } |
261 | |
262 | Color ColorWheel::color() const { |
263 | Color rgb = hue2rgb(mHue); |
264 | Color black { 0.f, 0.f, 0.f, 1.f }; |
265 | Color white { 1.f, 1.f, 1.f, 1.f }; |
266 | return rgb * (1 - mWhite - mBlack) + black * mBlack + white * mWhite; |
267 | } |
268 | |
269 | void ColorWheel::setColor(const Color &rgb) { |
270 | float r = rgb[0], g = rgb[1], b = rgb[2]; |
271 | |
272 | float max = std::max({ r, g, b }); |
273 | float min = std::min({ r, g, b }); |
274 | float l = (max + min) / 2; |
275 | |
276 | if (max == min) { |
277 | mHue = 0.; |
278 | mBlack = 1. - l; |
279 | mWhite = l; |
280 | } else { |
281 | float d = max - min, h; |
282 | /* float s = l > 0.5 ? d / (2 - max - min) : d / (max + min); */ |
283 | if (max == r) |
284 | h = (g - b) / d + (g < b ? 6 : 0); |
285 | else if (max == g) |
286 | h = (b - r) / d + 2; |
287 | else |
288 | h = (r - g) / d + 4; |
289 | h /= 6; |
290 | |
291 | mHue = h; |
292 | |
293 | Eigen::Matrix<float, 4, 3> M; |
294 | M.topLeftCorner<3, 1>() = hue2rgb(h).head<3>(); |
295 | M(3, 0) = 1.; |
296 | M.col(1) = Vector4f{ 0., 0., 0., 1. }; |
297 | M.col(2) = Vector4f{ 1., 1., 1., 1. }; |
298 | |
299 | Vector4f rgb4{ rgb[0], rgb[1], rgb[2], 1. }; |
300 | Vector3f bary = M.colPivHouseholderQr().solve(rgb4); |
301 | |
302 | mBlack = bary[1]; |
303 | mWhite = bary[2]; |
304 | } |
305 | } |
306 | |
307 | void ColorWheel::save(Serializer &s) const { |
308 | Widget::save(s); |
309 | s.set("hue" , mHue); |
310 | s.set("white" , mWhite); |
311 | s.set("black" , mBlack); |
312 | } |
313 | |
314 | bool ColorWheel::load(Serializer &s) { |
315 | if (!Widget::load(s)) return false; |
316 | if (!s.get("hue" , mHue)) return false; |
317 | if (!s.get("white" , mWhite)) return false; |
318 | if (!s.get("black" , mBlack)) return false; |
319 | mDragRegion = Region::None; |
320 | return true; |
321 | } |
322 | |
323 | NAMESPACE_END(nanogui) |
324 | |
325 | |