From 6b8681153218d0573f694de078268b388227c2d5 Mon Sep 17 00:00:00 2001 From: askmeaboutloom Date: Fri, 8 Nov 2024 16:46:53 +0100 Subject: [PATCH] Implement gamut masks for the color circle They get loaded from SVG files, titles are taken from the file. Currently no in-app way of managing them, but SVGs can be thrown into the application data and will be picked up from there. --- ChangeLog | 2 +- .../gamutmasks/atmospherewithaccent.svg | 44 +++++ .../assets/gamutmasks/atmospherictriad.svg | 34 ++++ .../assets/gamutmasks/complementary.svg | 34 ++++ .../gamutmasks/dominanthuewithaccent.svg | 46 ++++++ .../assets/gamutmasks/shiftedtriad.svg | 34 ++++ src/desktop/assets/gamutmasks/split.svg | 33 ++++ .../assets/gamutmasks/splitcomplementary.svg | 48 ++++++ src/desktop/assets/gamutmasks/tetradic.svg | 33 ++++ .../dialogs/artisticcolorwheeldialog.cpp | 152 +++++++++++++++++- .../dialogs/artisticcolorwheeldialog.h | 6 + src/desktop/docks/colorcircle.cpp | 4 + src/desktop/settings_table.h | 2 + src/desktop/widgets/artisticcolorwheel.cpp | 65 ++++++++ src/desktop/widgets/artisticcolorwheel.h | 9 ++ 15 files changed, 544 insertions(+), 2 deletions(-) create mode 100644 src/desktop/assets/gamutmasks/atmospherewithaccent.svg create mode 100644 src/desktop/assets/gamutmasks/atmospherictriad.svg create mode 100644 src/desktop/assets/gamutmasks/complementary.svg create mode 100644 src/desktop/assets/gamutmasks/dominanthuewithaccent.svg create mode 100644 src/desktop/assets/gamutmasks/shiftedtriad.svg create mode 100644 src/desktop/assets/gamutmasks/split.svg create mode 100644 src/desktop/assets/gamutmasks/splitcomplementary.svg create mode 100644 src/desktop/assets/gamutmasks/tetradic.svg diff --git a/ChangeLog b/ChangeLog index c77915e90c..f0bd06ae32 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,5 @@ Unreleased Version 2.2.2-pre - * Feature: Color circle dock. Similar to Krita's Artistic Color Selector and MyPaint's HSV/HCY Wheel. + * Feature: Color circle dock with gamut masks, available through View > Docks > Color Circle. Similar to Krita's Artistic Color Selector and MyPaint's HSV/HCY Wheel. 2024-11-06 Version 2.2.2-beta.4 * Fix: Solve rendering glitches with selection outlines that happen on some systems. Thanks xxxx for reporting. diff --git a/src/desktop/assets/gamutmasks/atmospherewithaccent.svg b/src/desktop/assets/gamutmasks/atmospherewithaccent.svg new file mode 100644 index 0000000000..04f7b3bed8 --- /dev/null +++ b/src/desktop/assets/gamutmasks/atmospherewithaccent.svg @@ -0,0 +1,44 @@ + + + Atmosphere with accent + + + + + + + Atmosphere with accent + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/atmospherictriad.svg b/src/desktop/assets/gamutmasks/atmospherictriad.svg new file mode 100644 index 0000000000..cffeef0312 --- /dev/null +++ b/src/desktop/assets/gamutmasks/atmospherictriad.svg @@ -0,0 +1,34 @@ + + + Atmospheric triad + + + + + + Atmospheric triad + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/complementary.svg b/src/desktop/assets/gamutmasks/complementary.svg new file mode 100644 index 0000000000..e13de85caf --- /dev/null +++ b/src/desktop/assets/gamutmasks/complementary.svg @@ -0,0 +1,34 @@ + + + Complementary + + + + + + Complementary + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/dominanthuewithaccent.svg b/src/desktop/assets/gamutmasks/dominanthuewithaccent.svg new file mode 100644 index 0000000000..e0e48ebe41 --- /dev/null +++ b/src/desktop/assets/gamutmasks/dominanthuewithaccent.svg @@ -0,0 +1,46 @@ + + + Dominant hue with accent + + + + + + + Dominant hue with accent + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/shiftedtriad.svg b/src/desktop/assets/gamutmasks/shiftedtriad.svg new file mode 100644 index 0000000000..fea24072cc --- /dev/null +++ b/src/desktop/assets/gamutmasks/shiftedtriad.svg @@ -0,0 +1,34 @@ + + + Shifted triad + + + + + + Shifted triad + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/split.svg b/src/desktop/assets/gamutmasks/split.svg new file mode 100644 index 0000000000..8ae5e23f63 --- /dev/null +++ b/src/desktop/assets/gamutmasks/split.svg @@ -0,0 +1,33 @@ + + + Split + + + + + Split + + + + + diff --git a/src/desktop/assets/gamutmasks/splitcomplementary.svg b/src/desktop/assets/gamutmasks/splitcomplementary.svg new file mode 100644 index 0000000000..003ce5269e --- /dev/null +++ b/src/desktop/assets/gamutmasks/splitcomplementary.svg @@ -0,0 +1,48 @@ + + + Split complementary + + + + + + + + Split complementary + Krita + + + + diff --git a/src/desktop/assets/gamutmasks/tetradic.svg b/src/desktop/assets/gamutmasks/tetradic.svg new file mode 100644 index 0000000000..95f45ad610 --- /dev/null +++ b/src/desktop/assets/gamutmasks/tetradic.svg @@ -0,0 +1,33 @@ + + + Tetradic + + + + + Tetradic + + + + + diff --git a/src/desktop/dialogs/artisticcolorwheeldialog.cpp b/src/desktop/dialogs/artisticcolorwheeldialog.cpp index d211085241..ee345f78c0 100644 --- a/src/desktop/dialogs/artisticcolorwheeldialog.cpp +++ b/src/desktop/dialogs/artisticcolorwheeldialog.cpp @@ -4,8 +4,13 @@ #include "desktop/utils/widgetutils.h" #include "desktop/widgets/artisticcolorwheel.h" #include "desktop/widgets/kis_slider_spin_box.h" +#include "libshared/util/paths.h" #include #include +#include +#include +#include +#include #include namespace dialogs { @@ -15,7 +20,7 @@ ArtisticColorWheelDialog::ArtisticColorWheelDialog(QWidget *parent) { setWindowTitle(tr("Color Circle Settings")); setWindowModality(Qt::WindowModal); - resize(350, 500); + resize(350, 550); QVBoxLayout *layout = new QVBoxLayout(this); const desktop::settings::Settings &settings = dpApp().settings(); @@ -115,6 +120,37 @@ ArtisticColorWheelDialog::ArtisticColorWheelDialog(QWidget *parent) } } + QString gamutMaskPath = settings.colorCircleGamutMaskPath(); + m_gamutMaskCombo = new QComboBox; + m_gamutMaskCombo->addItem(tr("No gamut mask")); + loadGamutMasks(); + if(!gamutMaskPath.isEmpty()) { + int count = m_gamutMaskCombo->count(); + for(int i = 1; i < count; ++i) { + if(m_gamutMaskCombo->itemData(i).toString() == gamutMaskPath) { + m_gamutMaskCombo->setCurrentIndex(i); + break; + } + } + } + layout->addWidget(m_gamutMaskCombo); + connect( + m_gamutMaskCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &ArtisticColorWheelDialog::updateGamutMask); + + int gamutMaskAngle = settings.colorCircleGamutMaskAngle(); + m_gamutMaskAngleSlider = new KisSliderSpinBox; + m_gamutMaskAngleSlider->setPrefix(tr("Mask angle: ")); + //: Degree symbol. Unless your language uses a different one, keep as-is. + m_gamutMaskAngleSlider->setSuffix(tr("°")); + m_gamutMaskAngleSlider->setRange(0, 359); + m_gamutMaskAngleSlider->setValue(gamutMaskAngle); + layout->addWidget(m_gamutMaskAngleSlider); + connect( + m_gamutMaskAngleSlider, + QOverload::of(&KisSliderSpinBox::valueChanged), this, + &ArtisticColorWheelDialog::updateGamutMaskAngle); + m_wheel = new widgets::ArtisticColorWheel; m_wheel->setHueLimit(hueLimit); m_wheel->setHueCount(hueCount); @@ -123,6 +159,8 @@ ArtisticColorWheelDialog::ArtisticColorWheelDialog(QWidget *parent) m_wheel->setSaturationCount(saturationCount); m_wheel->setValueLimit(valueLimit); m_wheel->setValueCount(valueCount); + m_wheel->setGamutMaskPath(gamutMaskPath); + m_wheel->setGamutMaskAngle(gamutMaskAngle); m_wheel->setColorSpace(colorSpace); m_wheel->setColor(Qt::red); m_wheel->setMinimumSize(64, 64); @@ -143,6 +181,103 @@ ArtisticColorWheelDialog::ArtisticColorWheelDialog(QWidget *parent) &ArtisticColorWheelDialog::saveSettings); } +void ArtisticColorWheelDialog::loadGamutMasks() +{ + QHash defaultGamutMaskTitles = { + { + QStringLiteral("Atmosphere with accent"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Atmosphere with accent"), + }, + { + QStringLiteral("Atmospheric triad"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Atmospheric triad"), + }, + { + QStringLiteral("Complementary"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Complementary"), + }, + { + QStringLiteral("Dominant hue with accent"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Dominant hue with accent"), + }, + { + QStringLiteral("Shifted triad"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Shifted triad"), + }, + { + QStringLiteral("Split"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Split"), + }, + { + QStringLiteral("Split complementary"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Split complementary"), + }, + { + QStringLiteral("Tetradic"), + //: This is the name for a gamut mask, a shape on a color circle. + tr("Tetradic"), + }, + }; + + QStringList datapaths = utils::paths::dataPaths(); + QVector> entries; + for(const QString &path : datapaths) { + QDir dir(path + QStringLiteral("/gamutmasks/")); + for(const QString &p : + dir.entryList({QStringLiteral("*.svg")}, QDir::Files)) { + QFile f(dir.filePath(p)); + if(!f.open(QIODevice::ReadOnly)) { + qWarning( + "Error opening gamut mask '%s': %s", + qUtf8Printable(f.fileName()), + qUtf8Printable(f.errorString())); + continue; + } + + QDomDocument doc; + QDomDocument::ParseResult parseResult = doc.setContent(&f); + f.close(); + if(!parseResult) { + qWarning( + "Error parsing gamut mask '%s': %s", + qUtf8Printable(f.fileName()), + qUtf8Printable(parseResult.errorMessage)); + continue; + } + + QString title; + QDomElement titleElement = doc.documentElement().firstChildElement( + QStringLiteral("title")); + if(!titleElement.isNull()) { + title = titleElement.text().trimmed(); + } + + if(title.isEmpty()) { + title = QFileInfo(f.fileName()).baseName(); + } + + entries.append( + {defaultGamutMaskTitles.value(title, title), f.fileName()}); + } + } + + std::sort( + entries.begin(), entries.end(), + [](const QPair &a, const QPair &b) { + return a.first.compare(b.first, Qt::CaseInsensitive) < 0; + }); + for(const QPair &entry : entries) { + m_gamutMaskCombo->addItem(entry.first, entry.second); + } +} + void ArtisticColorWheelDialog::updateContinuousHue(compat::CheckBoxState state) { bool limit = state == Qt::Unchecked; @@ -187,6 +322,18 @@ void ArtisticColorWheelDialog::updateValueSteps(int steps) m_wheel->setValueCount(steps); } +void ArtisticColorWheelDialog::updateGamutMask(int index) +{ + QString path = m_gamutMaskCombo->itemData(index).toString(); + m_wheel->setGamutMaskPath(path); + m_gamutMaskAngleSlider->setEnabled(!path.isEmpty()); +} + +void ArtisticColorWheelDialog::updateGamutMaskAngle(int angle) +{ + m_wheel->setGamutMaskAngle(angle); +} + void ArtisticColorWheelDialog::saveSettings() { desktop::settings::Settings &settings = dpApp().settings(); @@ -198,6 +345,9 @@ void ArtisticColorWheelDialog::saveSettings() settings.setColorCircleSaturationCount(m_saturationStepsSlider->value()); settings.setColorCircleValueLimit(!m_continuousValueBox->isChecked()); settings.setColorCircleValueCount(m_valueStepsSlider->value()); + settings.setColorCircleGamutMaskPath( + m_gamutMaskCombo->currentData().toString()); + settings.setColorCircleGamutMaskAngle(m_gamutMaskAngleSlider->value()); } } diff --git a/src/desktop/dialogs/artisticcolorwheeldialog.h b/src/desktop/dialogs/artisticcolorwheeldialog.h index 1564be21bc..1f8770324c 100644 --- a/src/desktop/dialogs/artisticcolorwheeldialog.h +++ b/src/desktop/dialogs/artisticcolorwheeldialog.h @@ -8,6 +8,7 @@ class KisSliderSpinBox; class QCheckBox; class QColor; +class QComboBox; namespace widgets { class ArtisticColorWheel; @@ -23,6 +24,7 @@ class ArtisticColorWheelDialog : public QDialog { explicit ArtisticColorWheelDialog(QWidget *parent = nullptr); private: + void loadGamutMasks(); void updateContinuousHue(compat::CheckBoxState state); void updateHueSteps(int steps); void updateHueAngle(int angle); @@ -30,6 +32,8 @@ class ArtisticColorWheelDialog : public QDialog { void updateSaturationSteps(int steps); void updateContinuousValue(compat::CheckBoxState state); void updateValueSteps(int steps); + void updateGamutMask(int index); + void updateGamutMaskAngle(int angle); void saveSettings(); QCheckBox *m_continuousHueBox; @@ -39,6 +43,8 @@ class ArtisticColorWheelDialog : public QDialog { KisSliderSpinBox *m_saturationStepsSlider; QCheckBox *m_continuousValueBox; KisSliderSpinBox *m_valueStepsSlider; + QComboBox *m_gamutMaskCombo; + KisSliderSpinBox *m_gamutMaskAngleSlider; widgets::ArtisticColorWheel *m_wheel; }; diff --git a/src/desktop/docks/colorcircle.cpp b/src/desktop/docks/colorcircle.cpp index 04d3ac7963..ea4de28afc 100644 --- a/src/desktop/docks/colorcircle.cpp +++ b/src/desktop/docks/colorcircle.cpp @@ -142,6 +142,10 @@ ColorCircleDock::ColorCircleDock(QWidget *parent) m_wheel, &widgets::ArtisticColorWheel::setValueLimit); settings.bindColorCircleValueCount( m_wheel, &widgets::ArtisticColorWheel::setValueCount); + settings.bindColorCircleGamutMaskPath( + m_wheel, &widgets::ArtisticColorWheel::setGamutMaskPath); + settings.bindColorCircleGamutMaskAngle( + m_wheel, &widgets::ArtisticColorWheel::setGamutMaskAngle); settings.bindColorWheelSpace(this, &ColorCircleDock::setColorSpace); #ifdef DP_COLOR_CIRCLE_ENABLE_PREVIEW settings.bindColorWheelPreview(this, &ColorCircleDock::setPreview); diff --git a/src/desktop/settings_table.h b/src/desktop/settings_table.h index aa6eb7d686..0b7a763cfb 100644 --- a/src/desktop/settings_table.h +++ b/src/desktop/settings_table.h @@ -96,6 +96,8 @@ SETTING(canvasShortcuts , CanvasShortcuts , "settings/canvas #ifdef Q_OS_ANDROID SETTING(captureVolumeRocker , CaptureVolumeRocker , "settings/android/capturevolumerocker" , true) #endif +SETTING(colorCircleGamutMaskAngle , ColorCircleGamutMaskAngle , "settings/colorcircle/gamutmaskangle" , 0) +SETTING(colorCircleGamutMaskPath , ColorCircleGamutMaskPath , "settings/colorcircle/gamutmaskpath" , QString()) SETTING(colorCircleHueAngle , ColorCircleHueAngle , "settings/colorcircle/hueangle" , 0) SETTING(colorCircleHueCount , ColorCircleHueCount , "settings/colorcircle/huecount" , 16) SETTING(colorCircleHueLimit , ColorCircleHueLimit , "settings/colorcircle/huelimit" , false) diff --git a/src/desktop/widgets/artisticcolorwheel.cpp b/src/desktop/widgets/artisticcolorwheel.cpp index 23a816a5e8..7804dc8948 100644 --- a/src/desktop/widgets/artisticcolorwheel.cpp +++ b/src/desktop/widgets/artisticcolorwheel.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -99,6 +100,45 @@ void ArtisticColorWheel::setValueCount(int valueCount) } } +void ArtisticColorWheel::setGamutMaskPath(const QString &gamutMaskPath) +{ + bool empty = gamutMaskPath.isEmpty(); + if(!empty || !m_gamutMaskPath.isEmpty()) { + m_gamutMaskPath = gamutMaskPath; + delete m_gamutMask; + + if(empty) { + m_gamutMask = nullptr; + } else { + m_gamutMask = new QSvgRenderer(gamutMaskPath); + if(!m_gamutMask->isValid()) { + qWarning( + "Error loading gamut mask '%s'", + qUtf8Printable(gamutMaskPath)); + delete m_gamutMask; + m_gamutMask = nullptr; + } + } + + m_wheelCache = QPixmap(); + m_gamutMaskCache = QPixmap(); + update(); + } +} + +void ArtisticColorWheel::setGamutMaskAngle(int gamutMaskAngle) +{ + qreal angle = normalizeAngle(qreal(gamutMaskAngle)); + if(m_gamutMaskAngle != angle) { + m_gamutMaskAngle = angle; + if(m_gamutMask) { + m_wheelCache = QPixmap(); + m_gamutMaskCache = QPixmap(); + update(); + } + } +} + void ArtisticColorWheel::setColorSpace(ColorSpace colorSpace) { switch(colorSpace) { @@ -362,6 +402,31 @@ void ArtisticColorWheel::updateWheelCache(int dimension) painter.setPen(Qt::NoPen); painter.setRenderHint(QPainter::Antialiasing); painter.drawEllipse(m_wheelCache.rect()); + + if(m_gamutMask) { + updateGamutMaskCache(m_wheelCache.size()); + painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); + painter.setOpacity(0.5); + painter.drawPixmap(m_wheelCache.rect(), m_gamutMaskCache); + } + } +} + +void ArtisticColorWheel::updateGamutMaskCache(const QSize &size) +{ + if(m_gamutMaskCache.isNull() || m_gamutMaskCache.size() != size) { + m_gamutMaskCache = QPixmap(size); + m_gamutMaskCache.fill(Qt::transparent); + QPainter painter(&m_gamutMaskCache); + if(m_gamutMaskAngle != 0.0) { + QTransform tf; + QPointF center = QRectF(m_gamutMaskCache.rect()).center(); + tf.translate(center.x(), center.y()); + tf.rotate(m_gamutMaskAngle); + tf.translate(-center.x(), -center.y()); + painter.setTransform(tf); + } + m_gamutMask->render(&painter, m_gamutMaskCache.rect()); } } diff --git a/src/desktop/widgets/artisticcolorwheel.h b/src/desktop/widgets/artisticcolorwheel.h index 1f07a95887..216110bc79 100644 --- a/src/desktop/widgets/artisticcolorwheel.h +++ b/src/desktop/widgets/artisticcolorwheel.h @@ -6,6 +6,8 @@ #include #include +class QSvgRenderer; + namespace widgets { class ArtisticColorWheel : public QWidget { @@ -31,6 +33,8 @@ class ArtisticColorWheel : public QWidget { void setSaturationCount(int saturationCount); void setValueLimit(bool valueLimit); void setValueCount(int valueCount); + void setGamutMaskPath(const QString &path); + void setGamutMaskAngle(int gamutMaskAngle); ColorSpace colorSpace() const { return m_colorSpace; } void setColorSpace(ColorSpace colorSpace); @@ -59,6 +63,7 @@ class ArtisticColorWheel : public QWidget { void handleMouseOnWheel(qreal radius, qreal length, qreal angle); void updateBarCache(const QSize &size); void updateWheelCache(int dimension); + void updateGamutMaskCache(const QSize &size); void updatePathCache(const QRectF &vr, const QRectF &wr); qreal getHueAt(qreal angle) const; @@ -87,9 +92,13 @@ class ArtisticColorWheel : public QWidget { qreal m_hue = 0.0; qreal m_saturation = 0.0; qreal m_value = 0.0; + qreal m_gamutMaskAngle = 0.0; ColorSpace m_colorSpace = ColorSpace::ColorHSV; + QString m_gamutMaskPath; + QSvgRenderer *m_gamutMask = nullptr; QPixmap m_barCache; QPixmap m_wheelCache; + QPixmap m_gamutMaskCache; QPainterPath m_pathCache; };