325 lines
8.9 KiB
C++
325 lines
8.9 KiB
C++
/*
|
|
src/colorwheel.cpp -- fancy analog widget to select a color value
|
|
|
|
This widget was contributed by Dmitriy Morozov.
|
|
|
|
NanoGUI was developed by Wenzel Jakob <wenzel@inf.ethz.ch>.
|
|
The widget drawing code is based on the NanoVG demo application
|
|
by Mikko Mononen.
|
|
|
|
All rights reserved. Use of this source code is governed by a
|
|
BSD-style license that can be found in the LICENSE.txt file.
|
|
*/
|
|
|
|
#include <nanogui/colorwheel.h>
|
|
#include <nanogui/theme.h>
|
|
#include <nanogui/opengl.h>
|
|
#include <nanogui/serializer/core.h>
|
|
#include <Eigen/QR>
|
|
#include <Eigen/Geometry>
|
|
|
|
NAMESPACE_BEGIN(nanogui)
|
|
|
|
ColorWheel::ColorWheel(Widget *parent, const Color& rgb)
|
|
: Widget(parent), mDragRegion(None) {
|
|
setColor(rgb);
|
|
}
|
|
|
|
Vector2i ColorWheel::preferredSize(NVGcontext *) const {
|
|
return { 100, 100. };
|
|
}
|
|
|
|
void ColorWheel::draw(NVGcontext *ctx) {
|
|
Widget::draw(ctx);
|
|
|
|
if (!mVisible)
|
|
return;
|
|
|
|
float x = mPos.x(),
|
|
y = mPos.y(),
|
|
w = mSize.x(),
|
|
h = mSize.y();
|
|
|
|
NVGcontext* vg = ctx;
|
|
|
|
int i;
|
|
float r0, r1, ax,ay, bx,by, cx,cy, aeps, r;
|
|
float hue = mHue;
|
|
NVGpaint paint;
|
|
|
|
nvgSave(vg);
|
|
|
|
cx = x + w*0.5f;
|
|
cy = y + h*0.5f;
|
|
r1 = (w < h ? w : h) * 0.5f - 5.0f;
|
|
r0 = r1 * .75f;
|
|
|
|
aeps = 0.5f / r1; // half a pixel arc length in radians (2pi cancels out).
|
|
|
|
for (i = 0; i < 6; i++) {
|
|
float a0 = (float)i / 6.0f * NVG_PI * 2.0f - aeps;
|
|
float a1 = (float)(i+1.0f) / 6.0f * NVG_PI * 2.0f + aeps;
|
|
nvgBeginPath(vg);
|
|
nvgArc(vg, cx,cy, r0, a0, a1, NVG_CW);
|
|
nvgArc(vg, cx,cy, r1, a1, a0, NVG_CCW);
|
|
nvgClosePath(vg);
|
|
ax = cx + cosf(a0) * (r0+r1)*0.5f;
|
|
ay = cy + sinf(a0) * (r0+r1)*0.5f;
|
|
bx = cx + cosf(a1) * (r0+r1)*0.5f;
|
|
by = cy + sinf(a1) * (r0+r1)*0.5f;
|
|
paint = nvgLinearGradient(vg, ax, ay, bx, by,
|
|
nvgHSLA(a0 / (NVG_PI * 2), 1.0f, 0.55f, 255),
|
|
nvgHSLA(a1 / (NVG_PI * 2), 1.0f, 0.55f, 255));
|
|
nvgFillPaint(vg, paint);
|
|
nvgFill(vg);
|
|
}
|
|
|
|
nvgBeginPath(vg);
|
|
nvgCircle(vg, cx,cy, r0-0.5f);
|
|
nvgCircle(vg, cx,cy, r1+0.5f);
|
|
nvgStrokeColor(vg, nvgRGBA(0,0,0,64));
|
|
nvgStrokeWidth(vg, 1.0f);
|
|
nvgStroke(vg);
|
|
|
|
// Selector
|
|
nvgSave(vg);
|
|
nvgTranslate(vg, cx,cy);
|
|
nvgRotate(vg, hue*NVG_PI*2);
|
|
|
|
// Marker on
|
|
float u = std::max(r1/50, 1.5f);
|
|
u = std::min(u, 4.f);
|
|
nvgStrokeWidth(vg, u);
|
|
nvgBeginPath(vg);
|
|
nvgRect(vg, r0-1,-2*u,r1-r0+2,4*u);
|
|
nvgStrokeColor(vg, nvgRGBA(255,255,255,192));
|
|
nvgStroke(vg);
|
|
|
|
paint = nvgBoxGradient(vg, r0-3,-5,r1-r0+6,10, 2,4, nvgRGBA(0,0,0,128), nvgRGBA(0,0,0,0));
|
|
nvgBeginPath(vg);
|
|
nvgRect(vg, r0-2-10,-4-10,r1-r0+4+20,8+20);
|
|
nvgRect(vg, r0-2,-4,r1-r0+4,8);
|
|
nvgPathWinding(vg, NVG_HOLE);
|
|
nvgFillPaint(vg, paint);
|
|
nvgFill(vg);
|
|
|
|
// Center triangle
|
|
r = r0 - 6;
|
|
ax = cosf(120.0f/180.0f*NVG_PI) * r;
|
|
ay = sinf(120.0f/180.0f*NVG_PI) * r;
|
|
bx = cosf(-120.0f/180.0f*NVG_PI) * r;
|
|
by = sinf(-120.0f/180.0f*NVG_PI) * r;
|
|
nvgBeginPath(vg);
|
|
nvgMoveTo(vg, r,0);
|
|
nvgLineTo(vg, ax, ay);
|
|
nvgLineTo(vg, bx, by);
|
|
nvgClosePath(vg);
|
|
paint = nvgLinearGradient(vg, r, 0, ax, ay, nvgHSLA(hue, 1.0f, 0.5f, 255),
|
|
nvgRGBA(255, 255, 255, 255));
|
|
nvgFillPaint(vg, paint);
|
|
nvgFill(vg);
|
|
paint = nvgLinearGradient(vg, (r + ax) * 0.5f, (0 + ay) * 0.5f, bx, by,
|
|
nvgRGBA(0, 0, 0, 0), nvgRGBA(0, 0, 0, 255));
|
|
nvgFillPaint(vg, paint);
|
|
nvgFill(vg);
|
|
nvgStrokeColor(vg, nvgRGBA(0, 0, 0, 64));
|
|
nvgStroke(vg);
|
|
|
|
// Select circle on triangle
|
|
float sx = r*(1 - mWhite - mBlack) + ax*mWhite + bx*mBlack;
|
|
float sy = ay*mWhite + by*mBlack;
|
|
|
|
nvgStrokeWidth(vg, u);
|
|
nvgBeginPath(vg);
|
|
nvgCircle(vg, sx,sy,2*u);
|
|
nvgStrokeColor(vg, nvgRGBA(255,255,255,192));
|
|
nvgStroke(vg);
|
|
|
|
nvgRestore(vg);
|
|
|
|
nvgRestore(vg);
|
|
}
|
|
|
|
bool ColorWheel::mouseButtonEvent(const Vector2i &p, int button, bool down,
|
|
int modifiers) {
|
|
Widget::mouseButtonEvent(p, button, down, modifiers);
|
|
if (!mEnabled || button != GLFW_MOUSE_BUTTON_1)
|
|
return false;
|
|
|
|
if (down) {
|
|
mDragRegion = adjustPosition(p);
|
|
return mDragRegion != None;
|
|
} else {
|
|
mDragRegion = None;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool ColorWheel::mouseDragEvent(const Vector2i &p, const Vector2i &,
|
|
int, int) {
|
|
return adjustPosition(p, mDragRegion) != None;
|
|
}
|
|
|
|
ColorWheel::Region ColorWheel::adjustPosition(const Vector2i &p, Region consideredRegions) {
|
|
float x = p.x() - mPos.x(),
|
|
y = p.y() - mPos.y(),
|
|
w = mSize.x(),
|
|
h = mSize.y();
|
|
|
|
float cx = w*0.5f;
|
|
float cy = h*0.5f;
|
|
float r1 = (w < h ? w : h) * 0.5f - 5.0f;
|
|
float r0 = r1 * .75f;
|
|
|
|
x -= cx;
|
|
y -= cy;
|
|
|
|
float mr = std::sqrt(x*x + y*y);
|
|
|
|
if ((consideredRegions & OuterCircle) &&
|
|
((mr >= r0 && mr <= r1) || (consideredRegions == OuterCircle))) {
|
|
if (!(consideredRegions & OuterCircle))
|
|
return None;
|
|
mHue = std::atan(y / x);
|
|
if (x < 0)
|
|
mHue += NVG_PI;
|
|
mHue /= 2*NVG_PI;
|
|
|
|
if (mCallback)
|
|
mCallback(color());
|
|
|
|
return OuterCircle;
|
|
}
|
|
|
|
float r = r0 - 6;
|
|
|
|
float ax = std::cos( 120.0f/180.0f*NVG_PI) * r;
|
|
float ay = std::sin( 120.0f/180.0f*NVG_PI) * r;
|
|
float bx = std::cos(-120.0f/180.0f*NVG_PI) * r;
|
|
float by = std::sin(-120.0f/180.0f*NVG_PI) * r;
|
|
|
|
typedef Eigen::Matrix<float,2,2> Matrix2f;
|
|
|
|
Eigen::Matrix<float, 2, 3> triangle;
|
|
triangle << ax,bx,r,
|
|
ay,by,0;
|
|
triangle = Eigen::Rotation2D<float>(mHue * 2 * NVG_PI).matrix() * triangle;
|
|
|
|
Matrix2f T;
|
|
T << triangle(0,0) - triangle(0,2), triangle(0,1) - triangle(0,2),
|
|
triangle(1,0) - triangle(1,2), triangle(1,1) - triangle(1,2);
|
|
Vector2f pos { x - triangle(0,2), y - triangle(1,2) };
|
|
|
|
Vector2f bary = T.colPivHouseholderQr().solve(pos);
|
|
float l0 = bary[0], l1 = bary[1], l2 = 1 - l0 - l1;
|
|
bool triangleTest = l0 >= 0 && l0 <= 1.f && l1 >= 0.f && l1 <= 1.f &&
|
|
l2 >= 0.f && l2 <= 1.f;
|
|
|
|
if ((consideredRegions & InnerTriangle) &&
|
|
(triangleTest || consideredRegions == InnerTriangle)) {
|
|
if (!(consideredRegions & InnerTriangle))
|
|
return None;
|
|
l0 = std::min(std::max(0.f, l0), 1.f);
|
|
l1 = std::min(std::max(0.f, l1), 1.f);
|
|
l2 = std::min(std::max(0.f, l2), 1.f);
|
|
float sum = l0 + l1 + l2;
|
|
l0 /= sum;
|
|
l1 /= sum;
|
|
mWhite = l0;
|
|
mBlack = l1;
|
|
if (mCallback)
|
|
mCallback(color());
|
|
return InnerTriangle;
|
|
}
|
|
|
|
return None;
|
|
}
|
|
|
|
Color ColorWheel::hue2rgb(float h) const {
|
|
float s = 1., v = 1.;
|
|
|
|
if (h < 0) h += 1;
|
|
|
|
int i = int(h * 6);
|
|
float f = h * 6 - i;
|
|
float p = v * (1 - s);
|
|
float q = v * (1 - f * s);
|
|
float t = v * (1 - (1 - f) * s);
|
|
|
|
float r = 0, g = 0, b = 0;
|
|
switch (i % 6) {
|
|
case 0: r = v, g = t, b = p; break;
|
|
case 1: r = q, g = v, b = p; break;
|
|
case 2: r = p, g = v, b = t; break;
|
|
case 3: r = p, g = q, b = v; break;
|
|
case 4: r = t, g = p, b = v; break;
|
|
case 5: r = v, g = p, b = q; break;
|
|
}
|
|
|
|
return { r, g, b, 1.f };
|
|
}
|
|
|
|
Color ColorWheel::color() const {
|
|
Color rgb = hue2rgb(mHue);
|
|
Color black { 0.f, 0.f, 0.f, 1.f };
|
|
Color white { 1.f, 1.f, 1.f, 1.f };
|
|
return rgb * (1 - mWhite - mBlack) + black * mBlack + white * mWhite;
|
|
}
|
|
|
|
void ColorWheel::setColor(const Color &rgb) {
|
|
float r = rgb[0], g = rgb[1], b = rgb[2];
|
|
|
|
float max = std::max({ r, g, b });
|
|
float min = std::min({ r, g, b });
|
|
float l = (max + min) / 2;
|
|
|
|
if (max == min) {
|
|
mHue = 0.;
|
|
mBlack = 1. - l;
|
|
mWhite = l;
|
|
} else {
|
|
float d = max - min, h;
|
|
/* float s = l > 0.5 ? d / (2 - max - min) : d / (max + min); */
|
|
if (max == r)
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
else if (max == g)
|
|
h = (b - r) / d + 2;
|
|
else
|
|
h = (r - g) / d + 4;
|
|
h /= 6;
|
|
|
|
mHue = h;
|
|
|
|
Eigen::Matrix<float, 4, 3> M;
|
|
M.topLeftCorner<3, 1>() = hue2rgb(h).head<3>();
|
|
M(3, 0) = 1.;
|
|
M.col(1) = Vector4f{ 0., 0., 0., 1. };
|
|
M.col(2) = Vector4f{ 1., 1., 1., 1. };
|
|
|
|
Vector4f rgb4{ rgb[0], rgb[1], rgb[2], 1. };
|
|
Vector3f bary = M.colPivHouseholderQr().solve(rgb4);
|
|
|
|
mBlack = bary[1];
|
|
mWhite = bary[2];
|
|
}
|
|
}
|
|
|
|
void ColorWheel::save(Serializer &s) const {
|
|
Widget::save(s);
|
|
s.set("hue", mHue);
|
|
s.set("white", mWhite);
|
|
s.set("black", mBlack);
|
|
}
|
|
|
|
bool ColorWheel::load(Serializer &s) {
|
|
if (!Widget::load(s)) return false;
|
|
if (!s.get("hue", mHue)) return false;
|
|
if (!s.get("white", mWhite)) return false;
|
|
if (!s.get("black", mBlack)) return false;
|
|
mDragRegion = Region::None;
|
|
return true;
|
|
}
|
|
|
|
NAMESPACE_END(nanogui)
|
|
|