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
21NAMESPACE_BEGIN(nanogui)
22
23ColorWheel::ColorWheel(Widget *parent, const Color& rgb)
24 : Widget(parent), mDragRegion(None) {
25 setColor(rgb);
26}
27
28Vector2i ColorWheel::preferredSize(NVGcontext *) const {
29 return { 100, 100. };
30}
31
32void 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
143bool 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
158bool ColorWheel::mouseDragEvent(const Vector2i &p, const Vector2i &,
159 int, int) {
160 return adjustPosition(p, mDragRegion) != None;
161}
162
163ColorWheel::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
238Color 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
262Color 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
269void 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
307void 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
314bool 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
323NAMESPACE_END(nanogui)
324
325