| 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 | |