File indexing completed on 2024-05-12 16:02:12

0001 /*
0002  *  SPDX-FileCopyrightText: 2008 Cyrille Berger <cberger@cberger.net>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.1-or-later
0005  */
0006 
0007 #include "KoTriangleColorSelector.h"
0008 #include <math.h>
0009 
0010 #include <QMouseEvent>
0011 #include <QPainter>
0012 #include <QPixmap>
0013 #include <QTimer>
0014 #include <KoColorSpaceRegistry.h>
0015 #include <KoColorConversions.h>
0016 #include <KoColorDisplayRendererInterface.h>
0017 
0018 
0019 enum CurrentHandle {
0020     NoHandle,
0021     HueHandle,
0022     ValueSaturationHandle };
0023 
0024 struct Q_DECL_HIDDEN KoTriangleColorSelector::Private {
0025     Private(KoTriangleColorSelector *_q, const KoColorDisplayRendererInterface *_displayRenderer)
0026         : q(_q),
0027           displayRenderer(_displayRenderer),
0028           lastX(-1),
0029           lastY(-1)
0030     {
0031     }
0032 
0033     KoTriangleColorSelector *q {nullptr};
0034     const KoColorDisplayRendererInterface *displayRenderer {nullptr};
0035     QPixmap wheelPixmap;
0036     QPixmap trianglePixmap;
0037     int hue {0};
0038     int saturation {0};
0039     int value {0};
0040     int sizeColorSelector {0};
0041     qreal centerColorSelector {0.0};
0042     qreal wheelWidthProportion {0.0};
0043     qreal wheelWidth {0.0};
0044     qreal wheelNormExt {0.0};
0045     qreal wheelNormInt {0.0};
0046     qreal wheelInnerRadius {0.0};
0047     qreal triangleRadius {0.0};
0048     qreal triangleLength {0.0};
0049     qreal triangleHeight {0.0};
0050     qreal triangleBottom {0.0};
0051     qreal triangleTop {0.0};
0052     qreal normExt {0.0};
0053     qreal normInt {0.0};
0054     bool updateAllowed {true};
0055     CurrentHandle handle {NoHandle};
0056     qreal triangleHandleSize {0.0};
0057     bool invalidTriangle {true};
0058     int lastX {-1};
0059     int lastY {-1};
0060     QTimer updateTimer;
0061 
0062     void init();
0063 };
0064 
0065 void KoTriangleColorSelector::Private::init()
0066 {
0067     q->setMinimumHeight( 100 );
0068     q->setMinimumWidth( 100 );
0069     q->setMouseTracking( true );
0070     q->updateTriangleCircleParameters();
0071     updateTimer.setInterval(1);
0072     updateTimer.setSingleShot(true);
0073     q->connect(&updateTimer, SIGNAL(timeout()), q, SLOT(update()));
0074 }
0075 
0076 KoTriangleColorSelector::KoTriangleColorSelector(QWidget* parent)
0077     : KisColorSelectorInterface(parent),
0078       d(new Private(this, KoDumbColorDisplayRenderer::instance()))
0079 {
0080     d->init();
0081 }
0082 
0083 KoTriangleColorSelector::KoTriangleColorSelector(const KoColorDisplayRendererInterface *displayRenderer, QWidget *parent)
0084     : KisColorSelectorInterface(parent),
0085       d(new Private(this, displayRenderer))
0086 {
0087     d->init();
0088     connect(displayRenderer, SIGNAL(displayConfigurationChanged()), this, SLOT(configurationChanged()), Qt::UniqueConnection);
0089 }
0090 
0091 KoTriangleColorSelector::~KoTriangleColorSelector()
0092 {
0093     delete d;
0094 }
0095 
0096 void KoTriangleColorSelector::updateTriangleCircleParameters()
0097 {
0098     d->sizeColorSelector = qMin(width(), height());
0099     d->centerColorSelector = 0.5 * d->sizeColorSelector;
0100     d->wheelWidthProportion = 0.25;
0101     d->wheelWidth = d->centerColorSelector * d->wheelWidthProportion;
0102     d->wheelNormExt = qAbs( d->centerColorSelector );
0103     d->wheelNormInt = qAbs( d->centerColorSelector * (1.0 - d->wheelWidthProportion));
0104     d->wheelInnerRadius = d->centerColorSelector * (1.0 - d->wheelWidthProportion);
0105     d->triangleRadius = d->wheelInnerRadius * 0.9;
0106     d->triangleLength = 3.0 / sqrt(3.0) * d->triangleRadius;
0107     d->triangleHeight = d->triangleLength * sqrt(3.0) * 0.5;
0108     d->triangleTop = 0.5 * d->sizeColorSelector - d->triangleRadius;
0109     d->triangleBottom = d->triangleHeight + d->triangleTop;
0110     d->triangleHandleSize = 10.0;
0111 }
0112 
0113 void KoTriangleColorSelector::paintEvent( QPaintEvent * event )
0114 {
0115 
0116     if( d->invalidTriangle )
0117     {
0118       generateTriangle();
0119     }
0120     Q_UNUSED(event);
0121     QPainter p(this);
0122     p.setRenderHint(QPainter::SmoothPixmapTransform);
0123     p.setRenderHint(QPainter::Antialiasing);
0124     QPointF pos(d->centerColorSelector, d->centerColorSelector);
0125     p.translate(QPointF( 0.5*width(), 0.5*height()  ) );
0126     // Draw the wheel
0127     p.drawPixmap( -pos, d->wheelPixmap );
0128     // Draw the triangle
0129     p.save();
0130 
0131     p.rotate( hue() + 150 );
0132 
0133 
0134     p.drawPixmap( -pos , d->trianglePixmap );
0135     // Draw selectors
0136     p.restore();
0137     // Draw value,saturation selector
0138     //   Compute coordinates
0139     {
0140         qreal vs_selector_ypos_ = value() / 255.0;
0141         qreal ls_ = (vs_selector_ypos_) * d->triangleLength; // length of the saturation on the triangle
0142         qreal vs_selector_xpos_ = ls_ * (saturation() / 255.0 - 0.5);
0143         // Draw it
0144         p.save();
0145         p.setPen( QPen( Qt::white, 1.0) );
0146 
0147         QColor currentColor = d->displayRenderer->toQColor(getCurrentColor());
0148 
0149         p.setBrush(currentColor);
0150         p.rotate( hue() + 150 );
0151         p.drawEllipse( QRectF( -d->triangleHandleSize*0.5 + vs_selector_xpos_,
0152                                -d->triangleHandleSize*0.5 - (d->centerColorSelector - d->triangleTop) + vs_selector_ypos_ * d->triangleHeight,
0153                                 d->triangleHandleSize , d->triangleHandleSize ));
0154     }
0155     p.restore();
0156     // Draw Hue selector
0157     p.save();
0158     p.setPen( QPen( Qt::white, 1.0) );
0159     p.rotate( hue() - 90 );
0160     qreal hueSelectorWidth_ = 0.8;
0161     qreal hueSelectorOffset_ = 0.5 *( 1.0 - hueSelectorWidth_) * d->wheelWidth;
0162     qreal hueSelectorSize_ = 0.8 * d->wheelWidth;
0163     p.drawRect( QRectF( -1.5, -d->centerColorSelector + hueSelectorOffset_, 3.0, hueSelectorSize_ ));
0164     p.restore();
0165     p.end();
0166 }
0167 
0168 
0169 // make sure to always use get/set functions when managing HSV properties (don't call directly like d->hue)
0170 // these settings get updated A LOT when the color sampler is being used. You might get unexpected results
0171 int KoTriangleColorSelector::hue() const
0172 {
0173     return d->hue;
0174 }
0175 
0176 void KoTriangleColorSelector::setHue(int h)
0177 {
0178     // setRealColor() will give you -1 when saturation is 0
0179     // ignore setting hue in this instance. otherwise it will mess up the hue ring
0180     if (h == -1)
0181         return;
0182 
0183 
0184     h = qBound(0, h, 359);
0185     d->hue = h;
0186     tellColorChanged();
0187     d->invalidTriangle = true;
0188     d->updateTimer.start();
0189 }
0190 
0191 int KoTriangleColorSelector::value() const
0192 {
0193     return d->value;
0194 }
0195 
0196 void KoTriangleColorSelector::setValue(int v)
0197 {
0198     v = qBound(0, v, 255);
0199     d->value = v;
0200     tellColorChanged();
0201     d->invalidTriangle = true;
0202     d->updateTimer.start();
0203 }
0204 
0205 int KoTriangleColorSelector::saturation() const
0206 {
0207     return d->saturation;
0208 }
0209 
0210 void KoTriangleColorSelector::setSaturation(int s)
0211 {
0212     s = qBound(0, s, 255);
0213     d->saturation = s;
0214     tellColorChanged();
0215     d->invalidTriangle = true;
0216     d->updateTimer.start();
0217 }
0218 
0219 void KoTriangleColorSelector::setHSV(int h, int s, int v)
0220 {
0221     d->invalidTriangle = (hue() != h);
0222     setHue(h);
0223     setValue(v);
0224     setSaturation(s);
0225 }
0226 
0227 KoColor KoTriangleColorSelector::getCurrentColor() const
0228 {
0229     return d->displayRenderer->fromHsv(hue(), saturation(), value());
0230 }
0231 
0232 void KoTriangleColorSelector::slotSetColor(const KoColor & color)
0233 {
0234     if ( getCurrentColor() == color)
0235         return;
0236 
0237     //displayrenderer->getHsv is what sets the foreground color in the application
0238     if(d->updateAllowed) {
0239         int hueRef = hue();
0240         int saturationRef = saturation();
0241         int valueRef = value();
0242 
0243         d->displayRenderer->getHsv(color, &hueRef, &saturationRef, &valueRef);
0244         setHSV(hueRef, saturationRef, valueRef);
0245 
0246         d->invalidTriangle = true;
0247         d->updateTimer.start();
0248     }
0249 }
0250 
0251 void KoTriangleColorSelector::resizeEvent( QResizeEvent * event )
0252 {
0253     QWidget::resizeEvent( event );
0254     updateTriangleCircleParameters();
0255     generateWheel();
0256     d->invalidTriangle = true;
0257 }
0258 
0259 inline qreal pow2(qreal v)
0260 {
0261     return v*v;
0262 }
0263 
0264 void KoTriangleColorSelector::tellColorChanged()
0265 {
0266     d->updateAllowed = false;
0267     emit(sigNewColor(getCurrentColor()));
0268     emit(colorChanged(getCurrentColor().toQColor()));
0269     d->updateAllowed = true;
0270 }
0271 
0272 void KoTriangleColorSelector::generateTriangle()
0273 {
0274     QSize size = QSize(1, 1)*d->sizeColorSelector*devicePixelRatioF(); // use when int needed
0275     QImage image(size, QImage::Format_ARGB32);
0276     image.setDevicePixelRatio(devicePixelRatioF());
0277 
0278     // Length of triangle
0279     int hue_ = hue();
0280 
0281     qreal triangleTop = d->triangleTop*devicePixelRatioF();
0282     qreal triangleBottom = d->triangleBottom*devicePixelRatioF();
0283 
0284     for(int y = 0; y < size.height(); ++y)
0285     {
0286         qreal ynormalize = ( triangleTop - y ) / ( triangleTop - triangleBottom );
0287         qreal v = 255 * ynormalize;
0288         qreal ls_ = (ynormalize) * d->triangleLength*devicePixelRatioF();
0289         qreal xStart = d->centerColorSelector*devicePixelRatioF() - 0.5 * ls_;
0290         qreal xEnd = xStart + ls_;
0291         qreal xMin = xStart - 1.0;
0292         qreal xMax = xEnd + 1.0;
0293         uint* data = reinterpret_cast<uint*>(image.scanLine(y));
0294         for(int x = 0; x < size.width(); ++x, ++data)
0295         {
0296             if (v < -1.0 || v > 256.0 || x < xMin || x > xMax)
0297             {
0298                 *data = qRgba(0,0,0,0);
0299             } else {
0300                 qreal s = 0.0;
0301                 qreal va = 1.0, sa = 1.0;
0302                 if( v < 0.0) { va = 1.0 + v; v = 0; }
0303                 else if( v > 255.0 ) { va = 256.0 - v; v = 255; }
0304 
0305                 if (x < xStart) {
0306                     sa = x - xMin;
0307                 } else if (x > xEnd) {
0308                     sa = xMax - x;
0309                     s = 255;
0310                 }
0311                 // avoid NaN values if we hit the triangle tip where ls_ is zero
0312                 // (and black has undefined saturation anyway)
0313                 else if (ls_ > 0.01) {
0314                     s = 255 * (x - xStart) / ls_;
0315                 }
0316                 qreal coeff = va * sa;
0317 
0318                 KoColor color = d->displayRenderer->fromHsv(hue_, s, v, int(coeff * 255.0));
0319                 QColor qcolor = d->displayRenderer->toQColor(color);
0320 
0321                 *data = qcolor.rgba();
0322             }
0323         }
0324     }
0325 
0326     d->trianglePixmap = QPixmap::fromImage(image);
0327     d->invalidTriangle = false;
0328 }
0329 
0330 void KoTriangleColorSelector::generateWheel()
0331 {
0332     QSize size = QSize(1, 1)*d->sizeColorSelector*devicePixelRatioF(); // use only when int needed
0333     QImage image(size, QImage::Format_ARGB32);
0334     image.setDevicePixelRatio(devicePixelRatioF());
0335 
0336     // the -0.5 ensures dimensions are respective to pixel centers and hence symmetrical
0337     qreal center = d->centerColorSelector*devicePixelRatioF() - 0.5;
0338     qreal wheelNormExt = d->wheelNormExt*devicePixelRatioF() - 0.5;
0339     qreal wheelNormInt = d->wheelNormInt*devicePixelRatioF() - 0.5;
0340 
0341 
0342     for(int y = 0; y < size.height(); y++)
0343     {
0344         qreal yc = y - center;
0345         qreal y2 = pow2( yc );
0346         for(int x = 0; x < size.width(); x++)
0347         {
0348             qreal xc = x - center;
0349             qreal norm = sqrt(pow2( xc ) + y2);
0350             if( norm <= wheelNormExt + 1.0 && norm >= wheelNormInt - 1.0 )
0351             {
0352                 qreal acoef = 1.0;
0353                 if(norm > wheelNormExt ) acoef = (1.0 + wheelNormExt - norm);
0354                 else if(norm < wheelNormInt ) acoef = (1.0 - wheelNormInt + norm);
0355                 qreal angle = atan2(yc, xc);
0356                 int h = (int)((180 * angle / M_PI) + 180) % 360;
0357 
0358                 KoColor color = d->displayRenderer->fromHsv(h, 255, 255, int(acoef * 255.0));
0359                 QColor qcolor = d->displayRenderer->toQColor(color);
0360 
0361                 image.setPixel(x,y, qcolor.rgba());
0362             } else {
0363                 image.setPixel(x,y, qRgba(0,0,0,0));
0364             }
0365         }
0366     }
0367     d->wheelPixmap = QPixmap::fromImage(image);
0368 }
0369 
0370 void KoTriangleColorSelector::mouseReleaseEvent( QMouseEvent * event )
0371 {
0372     if(event->button() == Qt::LeftButton)
0373     {
0374         selectColorAt( event->x(), event->y());
0375         d->handle = NoHandle;
0376     } else {
0377         QWidget::mouseReleaseEvent( event );
0378     }
0379 }
0380 
0381 void KoTriangleColorSelector::mousePressEvent( QMouseEvent * event )
0382 {
0383     if(event->button() == Qt::LeftButton)
0384     {
0385         d->handle = NoHandle;
0386         selectColorAt( event->x(), event->y());
0387     } else {
0388         QWidget::mousePressEvent( event );
0389     }
0390 }
0391 
0392 void KoTriangleColorSelector::mouseMoveEvent( QMouseEvent * event )
0393 {
0394     if(event->buttons() & Qt::LeftButton)
0395     {
0396         selectColorAt( event->x(), event->y(), false );
0397     } else {
0398         QWidget::mouseMoveEvent( event);
0399     }
0400 }
0401 
0402 void KoTriangleColorSelector::selectColorAt(int _x, int _y, bool checkInWheel)
0403 {
0404     Q_UNUSED( checkInWheel );
0405 
0406     if (d->lastX == _x && d->lastY == _y)
0407     {
0408         return;
0409     }
0410     d->lastX = _x;
0411     d->lastY = _y;
0412 
0413     qreal x = _x - 0.5*width();
0414     qreal y = _y - 0.5*height();
0415     // Check if the click is inside the wheel
0416     qreal norm = sqrt( x * x + y * y);
0417     if ( ( (norm < d->wheelNormExt) && (norm > d->wheelNormInt) && d->handle == NoHandle )
0418          || d->handle == HueHandle ) {
0419         d->handle = HueHandle;
0420         setHue( (int)(atan2(y, x) * 180 / M_PI ) + 180);
0421         d->updateTimer.start();
0422     }
0423     else {
0424     // Compute the s and v value, if they are in range, use them
0425         qreal rotation = -(hue() + 150) * M_PI / 180;
0426         qreal cr = cos(rotation);
0427         qreal sr = sin(rotation);
0428         qreal x1 = x * cr - y * sr; // <- now x1 gives the saturation
0429         qreal y1 = x * sr + y * cr; // <- now y1 gives the value
0430         y1 += d->wheelNormExt;
0431         qreal ynormalize = (d->triangleTop - y1 ) / ( d->triangleTop - d->triangleBottom );
0432         if( (ynormalize >= 0.0 && ynormalize <= 1.0 ) || d->handle == ValueSaturationHandle)
0433         {
0434             d->handle = ValueSaturationHandle;
0435             qreal ls_ = (ynormalize) * d->triangleLength; // length of the saturation on the triangle
0436             qreal sat = ( x1 / ls_ + 0.5) ;
0437             if((sat >= 0.0 && sat <= 1.0) || d->handle == ValueSaturationHandle)
0438             {
0439                 setHSV( hue(), sat * 255, ynormalize * 255);
0440             }
0441         }
0442         d->updateTimer.start();
0443     }
0444 }
0445 
0446 void KoTriangleColorSelector::configurationChanged()
0447 {
0448     generateWheel();
0449     d->invalidTriangle = true;
0450     update();
0451 }