File indexing completed on 2024-05-26 04:32:50

0001 /*
0002  * SPDX-FileCopyrightText: 2021 Mathias Wein <lynx.mw+kde@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  *
0006  * Code based on kis_my_paint_shade_selector.cpp from Advanced Color Selector,
0007  * which in turn is based on "lib/colorchanger_crossed_bowl.hpp" from MyPaint (mypaint.org),
0008  *
0009  * SPDX-FileCopyrightText: 2010 Adam Celarek <kdedev at xibo dot at>
0010  * SPDX-FileCopyrightText: 2008 Martin Renold <martinxyz@gmx.ch>
0011  * SPDX-FileCopyrightText: 2009 Ilya Portnov <nomail>
0012  */
0013 
0014 #include "WGMyPaintShadeSelector.h"
0015 
0016 #include <kis_display_color_converter.h>
0017 #include <kis_paint_device.h>
0018 #include <kis_painter.h>
0019 #include <kis_sequential_iterator.h>
0020 
0021 #include <QMouseEvent>
0022 #include <QPainter>
0023 #include <QVector4D>
0024 
0025 #include <cmath>
0026 
0027 template<class Iterator>
0028 void setColorWithIterator(Iterator &it, const KoColor &color, const int pixelSize) {
0029     memcpy(it.rawData(), color.data(), pixelSize);
0030 }
0031 
0032 inline int sqr(int x) {
0033     return x*x;
0034 }
0035 
0036 inline qreal sqr2(qreal x) {
0037     return (x*x + x)*0.5;
0038 }
0039 
0040 inline int signedSqr(int x) {
0041     return (x > 0) ? x*x : -(x*x);
0042 }
0043 
0044 WGMyPaintShadeSelector::WGMyPaintShadeSelector(WGSelectorDisplayConfigSP displayConfig, QWidget *parent, UiMode mode)
0045     : WGSelectorWidgetBase(displayConfig, parent, mode)
0046 {
0047     recalculateSizeHD();
0048 }
0049 
0050 WGMyPaintShadeSelector::~WGMyPaintShadeSelector()
0051 {
0052 
0053 }
0054 
0055 void WGMyPaintShadeSelector::setModel(KisVisualColorModelSP model)
0056 {
0057     if (m_model) {
0058         disconnect(m_model.data());
0059         m_model->disconnect(this);
0060     }
0061     m_model = model;
0062     connect(this, SIGNAL(sigChannelValuesChanged(QVector4D)),
0063             m_model.data(), SLOT(slotSetChannelValues(QVector4D)));
0064     connect(m_model.data(), SIGNAL(sigChannelValuesChanged(QVector4D,quint32)),
0065             this, SLOT(slotSetChannelValues(QVector4D)));
0066     if (m_model->isHSXModel()) {
0067         slotSetChannelValues(m_model->channelValues());
0068     }
0069 }
0070 
0071 void WGMyPaintShadeSelector::mousePressEvent(QMouseEvent *event)
0072 {
0073     if (event->button() == Qt::LeftButton) {
0074         emit sigColorInteraction(true);
0075         pickColorAt(event->localPos());
0076     } else {
0077         event->ignore();
0078     }
0079 }
0080 
0081 void WGMyPaintShadeSelector::mouseMoveEvent(QMouseEvent *event)
0082 {
0083     if (event->buttons() & Qt::LeftButton) {
0084         if (rect().contains(event->pos())) {
0085             pickColorAt(event->localPos());
0086         }
0087     } else {
0088         event->ignore();
0089     }
0090 }
0091 
0092 void WGMyPaintShadeSelector::mouseReleaseEvent(QMouseEvent *event)
0093 {
0094     if (event->button() == Qt::LeftButton) {
0095         emit sigColorInteraction(false);
0096     } else {
0097         event->ignore();
0098     }
0099 }
0100 
0101 void WGMyPaintShadeSelector::paintEvent(QPaintEvent *)
0102 {
0103     // Hint to the casual reader: some of the calculation here do not
0104     // what Martin Renold originally intended. Not everything here will make sense.
0105     // It does not matter in the end, as long as the result looks good.
0106     if (!m_model || !m_model->isHSXModel()) {
0107         return;
0108     }
0109 
0110     // This selector was ported from MyPaint in 2010
0111     if (!m_realPixelCache || m_realPixelCache->colorSpace() != m_model->colorSpace()) {
0112         m_realPixelCache = new KisPaintDevice(m_model->colorSpace());
0113         m_realCircleBorder = new KisPaintDevice(m_model->colorSpace());
0114 //        m_cachedColorSpace = colorSpace();
0115     }
0116     else {
0117         m_realPixelCache->clear();
0118         m_realCircleBorder->clear();
0119     }
0120 
0121     const int pixelSize = m_model->colorSpace()->pixelSize();
0122 
0123     QRect pickRectHighDPI = QRect(QPoint(0, 0), size()*devicePixelRatioF());
0124     KisSequentialIterator it(m_realPixelCache, pickRectHighDPI);
0125     KisSequentialIterator borderIt(m_realCircleBorder, pickRectHighDPI);
0126     QVector4D values;
0127     QVector4D values2;
0128 
0129     while (it.nextPixel() && borderIt.nextPixel()) {
0130         const int x = it.x();
0131         const int y = it.y();
0132 
0133         bool needsBlending = getChannelValues(QPoint(x, y), values, values2);
0134 
0135         if (needsBlending) {
0136             const qreal aaFactor = static_cast<qreal>(values2[3]);
0137             KoColor color = m_model->convertChannelValuesToKoColor(values2);
0138             color.setOpacity(aaFactor);
0139             setColorWithIterator(borderIt, color, pixelSize);
0140         }
0141 
0142         KoColor color = m_model->convertChannelValuesToKoColor(values);
0143         setColorWithIterator(it, color, pixelSize);
0144     }
0145 
0146     KisPainter gc(m_realPixelCache);
0147     gc.bitBlt(QPoint(0,0), m_realCircleBorder, QRect(rect().topLeft(), rect().size()*devicePixelRatioF()));
0148 
0149     QPainter painter(this);
0150     QImage renderedImage = displayConverter()->toQImage(m_realPixelCache, displayConfiguration()->previewInPaintingCS());
0151     renderedImage.setDevicePixelRatio(devicePixelRatioF());
0152 
0153     painter.drawImage(0, 0, renderedImage);
0154 }
0155 
0156 void WGMyPaintShadeSelector::resizeEvent(QResizeEvent *event)
0157 {
0158     WGSelectorWidgetBase::resizeEvent(event);
0159     recalculateSizeHD();
0160 }
0161 
0162 bool WGMyPaintShadeSelector::getChannelValues(QPoint pos, QVector4D &values, QVector4D &blendValues)
0163 {
0164     bool needsBlending = false;
0165 
0166     const float v_factor = 0.6f;
0167     const float s_factor = 0.6f;
0168     const float v_factor2 = 0.013f;
0169     const float s_factor2 = 0.013f;
0170 
0171     const int stripe_width = (15 * m_sizeHD)/255;
0172     int s_radiusHD = m_sizeHD/2.6;
0173 
0174     float h = 0;
0175     float s = 0;
0176     float v = 0;
0177 
0178     int dx = pos.x() - m_widthHD/2;
0179     int dy = pos.y() - m_heightHD/2;
0180     int diag = sqrt(2.0) * m_sizeHD/2;
0181 
0182     int dxs, dys;
0183     if (dx > 0)
0184         dxs = dx - stripe_width;
0185     else
0186         dxs = dx + stripe_width;
0187     if (dy > 0)
0188         dys = dy - stripe_width;
0189     else
0190         dys = dy + stripe_width;
0191 
0192     qreal r = std::sqrt(qreal(sqr(dxs)+sqr(dys)));
0193 
0194     if (qMin(abs(dx), abs(dy)) < stripe_width) {
0195         // horizontal and vertical lines
0196         bool horizontal = std::abs(dx) > std::abs(dy);
0197         dx = (dx/qreal(m_sizeHD))*255;
0198         dy = (dy/qreal(m_sizeHD))*255;
0199 
0200         h = 0;
0201         // x-axis = value, y-axis = saturation
0202         v =    dx*v_factor + signedSqr(dx)*v_factor2;
0203         s = - (dy*s_factor + signedSqr(dy)*s_factor2);
0204         // but not both at once
0205         if (horizontal) {
0206             // horizontal stripe
0207             s = 0.0;
0208         } else {
0209             // vertical stripe
0210             v = 0.0;
0211         }
0212     }
0213     else if (std::min(std::abs(dx - dy), std::abs(dx + dy)) < stripe_width) {
0214 
0215         dx = (dx/qreal(m_sizeHD))*255;
0216         dy = (dy/qreal(m_sizeHD))*255;
0217 
0218         h = 0;
0219         // x-axis = value, y-axis = saturation
0220         v =    dx*v_factor + signedSqr(dx)*v_factor2;
0221         s = - (dy*s_factor + signedSqr(dy)*s_factor2);
0222         // both at once
0223     }
0224     else if (r < s_radiusHD+1) {
0225 
0226         // hue
0227         if (dx > 0)
0228             h = 90*sqr2(r/s_radiusHD);
0229         else
0230             h = 360 - 90*sqr2(r/s_radiusHD);
0231         s = 256*(atan2f(std::abs(dxs),dys)/M_PI) - 128;
0232 
0233         if (r > s_radiusHD) {
0234             needsBlending = true;
0235             // antialiasing boarder
0236             qreal aaFactor = r-floor(r); // part after the decimal point
0237             aaFactor = 1-aaFactor;
0238 
0239             qreal fh = m_colorH + h/360.0;
0240             qreal fs = m_colorS + s/255.0;
0241             qreal fv = m_colorV + v/255.0;
0242 
0243             fh -= floor(fh);
0244             fs = qBound(qreal(0.0), fs, qreal(1.0));
0245             fv = qBound(qreal(0.01), fv, qreal(1.0));
0246             blendValues = QVector4D(fh, fs, fv, aaFactor);
0247 
0248             h = 180 + 180*atan2f(dys,-dxs)/M_PI;
0249             v = 255*(r-s_radiusHD)/(diag-s_radiusHD) - 128;
0250             s = 0; // overwrite the s value that was meant for the inside of the circle
0251             // here we already have drawn the inside, and the value left should be just the background value
0252         }
0253     }
0254     else {
0255         // background (hue+darkness gradient)
0256         h = 180 + 180*atan2f(dys,-dxs)/M_PI;
0257         v = 255*(r-s_radiusHD)/(diag-s_radiusHD) - 128;
0258     }
0259 
0260     qreal fh = m_colorH + h/360.0;
0261     qreal fs = m_colorS + s/255.0;
0262     qreal fv = m_colorV + v/255.0;
0263 
0264     fh -= floor(fh);
0265     fs = qBound(qreal(0.0), fs, qreal(1.0));
0266     fv = qBound(qreal(0.01), fv, qreal(1.0));
0267     values = QVector4D(fh, fs, fv, 0);
0268 
0269     return needsBlending;
0270 }
0271 
0272 void WGMyPaintShadeSelector::pickColorAt(const QPointF &posF)
0273 {
0274     QPoint pos = (posF * devicePixelRatioF()).toPoint();
0275     QVector4D values, dummy;
0276     getChannelValues(pos, values, dummy);
0277     m_allowUpdates = false;
0278     emit sigChannelValuesChanged(values);
0279     m_allowUpdates = true;
0280 }
0281 
0282 void WGMyPaintShadeSelector::recalculateSizeHD()
0283 {
0284     m_widthHD = qMax(1, width()) * devicePixelRatioF();
0285     m_heightHD = qMax(1, height()) *devicePixelRatioF();
0286     m_sizeHD = qMin(m_widthHD, m_heightHD);
0287 }
0288 
0289 void WGMyPaintShadeSelector::slotSetChannelValues(const QVector4D &values)
0290 {
0291     if (m_allowUpdates) {
0292         m_colorH = values.x();
0293         m_colorS = values.y();
0294         m_colorV = values.z();
0295         update();
0296     }
0297 }