Skip to content

Commit acabfb3

Browse files
committedNov 10, 2021
Add support for setting the color model to use when interpolating a
gradient ramp Some gradients will pass through murky grey mid-tones when interpolating using red/green/blue channels. Interpolating via hue/saturation/lightness/value channels can avoid these desaturated mid tones, resulting in more visually pleasing gradients. The color spec can be set per stop in a multi-stop gradient, and there's also an option to control the direction which interpolation should follow for the Hue component of a HSL/HSV color spec interpolation. Sponsored by North Road, thanks to SLYR
1 parent aaca87c commit acabfb3

File tree

10 files changed

+770
-54
lines changed

10 files changed

+770
-54
lines changed
 

‎python/core/auto_generated/qgscolorrampimpl.sip.in

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010

11+
1112
class QgsGradientStop
1213
{
1314
%Docstring(signature="appended")
@@ -33,6 +34,61 @@ Constructor for QgsGradientStop
3334
QColor color;
3435

3536
bool operator==( const QgsGradientStop &other ) const;
37+
38+
bool operator!=( const QgsGradientStop &other ) const;
39+
40+
QColor::Spec colorSpec() const;
41+
%Docstring
42+
Returns the color specification in which the color component interpolation will occur.
43+
44+
For multi-stop gradients this color spec will be used for the portion of the color ramp
45+
leading into the current stop.
46+
47+
.. seealso:: :py:func:`setColorSpec`
48+
49+
.. versionadded:: 3.24
50+
%End
51+
52+
void setColorSpec( QColor::Spec spec );
53+
%Docstring
54+
Sets the color specification in which the color component interpolation will occur.
55+
56+
Only QColor.Spec.Rgb, QColor.Spec.Hsv and QColor.Spec.Hsl are currently supported.
57+
58+
For multi-stop gradients this color spec will be used for the portion of the color ramp
59+
leading into the current stop.
60+
61+
.. seealso:: :py:func:`colorSpec`
62+
63+
.. versionadded:: 3.24
64+
%End
65+
66+
Qgis::AngularDirection direction() const;
67+
%Docstring
68+
Returns the direction to traverse the color wheel using when interpolating hue-based color
69+
specifications.
70+
71+
For multi-stop gradients this direction will be used for the portion of the color ramp
72+
leading into the current stop.
73+
74+
.. seealso:: :py:func:`setDirection`
75+
76+
.. versionadded:: 3.24
77+
%End
78+
79+
void setDirection( Qgis::AngularDirection direction );
80+
%Docstring
81+
Sets the ``direction`` to traverse the color wheel using when interpolating hue-based color
82+
specifications.
83+
84+
For multi-stop gradients this direction will be used for the portion of the color ramp
85+
leading into the current stop.
86+
87+
.. seealso:: :py:func:`direction`
88+
89+
.. versionadded:: 3.24
90+
%End
91+
3692
};
3793

3894
typedef QList<QgsGradientStop> QgsGradientStopsList;
@@ -207,9 +263,62 @@ Copy color ramp stops to a QGradient
207263
by this factor before adding to the gradient.
208264

209265
.. versionadded:: 2.1
266+
%End
267+
268+
QColor::Spec colorSpec() const;
269+
%Docstring
270+
Returns the color specification in which the color component interpolation will occur.
271+
272+
For multi-stop gradients this color spec will be used for the portion of the color ramp
273+
leading into the final stop (i.e. :py:func:`~QgsGradientColorRamp.color2`).
274+
275+
.. seealso:: :py:func:`setColorSpec`
276+
277+
.. versionadded:: 3.24
278+
%End
279+
280+
void setColorSpec( QColor::Spec spec );
281+
%Docstring
282+
Sets the color specification in which the color component interpolation will occur.
283+
284+
Only QColor.Spec.Rgb, QColor.Spec.Hsv and QColor.Spec.Hsl are currently supported.
285+
286+
For multi-stop gradients this color spec will be used for the portion of the color ramp
287+
leading into the final stop (i.e. :py:func:`~QgsGradientColorRamp.color2`).
288+
289+
.. seealso:: :py:func:`colorSpec`
290+
291+
.. versionadded:: 3.24
292+
%End
293+
294+
Qgis::AngularDirection direction() const;
295+
%Docstring
296+
Returns the direction to traverse the color wheel using when interpolating hue-based color
297+
specifications.
298+
299+
For multi-stop gradients this direction will be used for the portion of the color ramp
300+
leading into the final stop (i.e. :py:func:`~QgsGradientColorRamp.color2`).
301+
302+
.. seealso:: :py:func:`setDirection`
303+
304+
.. versionadded:: 3.24
305+
%End
306+
307+
void setDirection( Qgis::AngularDirection direction );
308+
%Docstring
309+
Sets the ``direction`` to traverse the color wheel using when interpolating hue-based color
310+
specifications.
311+
312+
For multi-stop gradients this direction will be used for the portion of the color ramp
313+
leading into the final stop (i.e. :py:func:`~QgsGradientColorRamp.color2`).
314+
315+
.. seealso:: :py:func:`direction`
316+
317+
.. versionadded:: 3.24
210318
%End
211319

212320
protected:
321+
213322
};
214323

215324

‎python/gui/auto_generated/qgsgradientstopeditor.sip.in

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ first or last stop is selected, as they cannot be repositioned.
9696
.. seealso:: :py:func:`setSelectedStopColor`
9797

9898
.. seealso:: :py:func:`setSelectedStopDetails`
99+
%End
100+
101+
void setSelectedStopColorSpec( QColor::Spec spec );
102+
%Docstring
103+
Sets the color ``spec`` for the current selected stop.
104+
105+
.. versionadded:: 3.24
106+
%End
107+
108+
void setSelectedStopDirection( Qgis::AngularDirection direction );
109+
%Docstring
110+
Sets the hue angular direction for the current selected stop.
111+
112+
.. versionadded:: 3.24
99113
%End
100114

101115
void setSelectedStopDetails( const QColor &color, double offset );

‎src/core/qgscolorrampimpl.cpp

Lines changed: 251 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,170 @@
3030

3131
//////////////
3232

33-
static QColor _interpolate( const QColor &c1, const QColor &c2, const double value )
33+
34+
static QColor _interpolateRgb( const QColor &c1, const QColor &c2, const double value, const Qgis::AngularDirection )
3435
{
35-
if ( std::isnan( value ) ) return c2;
36+
if ( std::isnan( value ) )
37+
return c2;
38+
39+
const qreal red1 = c1.redF();
40+
const qreal red2 = c2.redF();
41+
const qreal red = ( red1 + value * ( red2 - red1 ) );
42+
43+
const qreal green1 = c1.greenF();
44+
const qreal green2 = c2.greenF();
45+
const qreal green = ( green1 + value * ( green2 - green1 ) );
46+
47+
const qreal blue1 = c1.blueF();
48+
const qreal blue2 = c2.blueF();
49+
const qreal blue = ( blue1 + value * ( blue2 - blue1 ) );
3650

37-
qreal r = ( c1.redF() + value * ( c2.redF() - c1.redF() ) );
38-
qreal g = ( c1.greenF() + value * ( c2.greenF() - c1.greenF() ) );
39-
qreal b = ( c1.blueF() + value * ( c2.blueF() - c1.blueF() ) );
40-
qreal a = ( c1.alphaF() + value * ( c2.alphaF() - c1.alphaF() ) );
51+
const qreal alpha1 = c1.alphaF();
52+
const qreal alpha2 = c2.alphaF();
53+
const qreal alpha = ( alpha1 + value * ( alpha2 - alpha1 ) );
4154

42-
return QColor::fromRgbF( r, g, b, a );
55+
return QColor::fromRgbF( red, green, blue, alpha );
56+
}
57+
58+
static QColor _interpolateHsv( const QColor &c1, const QColor &c2, const double value, const Qgis::AngularDirection direction )
59+
{
60+
if ( std::isnan( value ) )
61+
return c2;
62+
63+
qreal hue1 = c1.hsvHueF();
64+
qreal hue2 = c2.hsvHueF();
65+
qreal hue;
66+
switch ( direction )
67+
{
68+
case Qgis::AngularDirection::Clockwise:
69+
{
70+
if ( hue1 < hue2 )
71+
hue1 += 1;
72+
73+
hue = hue1 - value * ( hue1 - hue2 );
74+
if ( hue < 0 )
75+
hue += 1;
76+
if ( hue > 1 )
77+
hue -= 1;
78+
break;
79+
}
80+
81+
case Qgis::AngularDirection::CounterClockwise:
82+
{
83+
if ( hue2 < hue1 )
84+
hue2 += 1;
85+
86+
hue = hue1 + value * ( hue2 - hue1 );
87+
if ( hue > 1 )
88+
hue -= 1;
89+
break;
90+
}
91+
}
92+
93+
const qreal saturation1 = c1.hsvSaturationF();
94+
const qreal saturation2 = c2.hsvSaturationF();
95+
const qreal saturation = ( saturation1 + value * ( saturation2 - saturation1 ) );
96+
97+
const qreal value1 = c1.valueF();
98+
const qreal value2 = c2.valueF();
99+
const qreal valueOut = ( value1 + value * ( value2 - value1 ) );
100+
101+
const qreal alpha1 = c1.alphaF();
102+
const qreal alpha2 = c2.alphaF();
103+
const qreal alpha = ( alpha1 + value * ( alpha2 - alpha1 ) );
104+
105+
return QColor::fromHsvF( hue > 1 ? hue - 1 : hue, saturation, valueOut, alpha );
106+
}
107+
108+
static QColor _interpolateHsl( const QColor &c1, const QColor &c2, const double value, const Qgis::AngularDirection direction )
109+
{
110+
if ( std::isnan( value ) )
111+
return c2;
112+
113+
qreal hue1 = c1.hslHueF();
114+
qreal hue2 = c2.hslHueF();
115+
qreal hue;
116+
switch ( direction )
117+
{
118+
case Qgis::AngularDirection::Clockwise:
119+
{
120+
if ( hue1 < hue2 )
121+
hue1 += 1;
122+
123+
hue = hue1 - value * ( hue1 - hue2 );
124+
if ( hue < 0 )
125+
hue += 1;
126+
if ( hue > 1 )
127+
hue -= 1;
128+
break;
129+
}
130+
131+
case Qgis::AngularDirection::CounterClockwise:
132+
{
133+
if ( hue2 < hue1 )
134+
hue2 += 1;
135+
136+
hue = hue1 + value * ( hue2 - hue1 );
137+
if ( hue > 1 )
138+
hue -= 1;
139+
break;
140+
}
141+
}
142+
143+
const qreal saturation1 = c1.hslSaturationF();
144+
const qreal saturation2 = c2.hslSaturationF();
145+
const qreal saturation = ( saturation1 + value * ( saturation2 - saturation1 ) );
146+
147+
const qreal lightness1 = c1.lightnessF();
148+
const qreal lightness2 = c2.lightnessF();
149+
const qreal lightness = ( lightness1 + value * ( lightness2 - lightness1 ) );
150+
151+
const qreal alpha1 = c1.alphaF();
152+
const qreal alpha2 = c2.alphaF();
153+
const qreal alpha = ( alpha1 + value * ( alpha2 - alpha1 ) );
154+
155+
return QColor::fromHslF( hue > 1 ? hue - 1 : hue, saturation, lightness, alpha );
43156
}
44157

45158
//////////////
46159

160+
161+
QgsGradientStop::QgsGradientStop( double offset, const QColor &color )
162+
: offset( offset )
163+
, color( color )
164+
, mFunc( _interpolateRgb )
165+
{
166+
167+
}
168+
169+
void QgsGradientStop::setColorSpec( QColor::Spec spec )
170+
{
171+
mColorSpec = spec;
172+
173+
switch ( mColorSpec )
174+
{
175+
case QColor::Rgb:
176+
case QColor::Invalid:
177+
case QColor::ExtendedRgb:
178+
case QColor::Cmyk:
179+
mFunc = _interpolateRgb;
180+
break;
181+
case QColor::Hsv:
182+
mFunc = _interpolateHsv;
183+
break;
184+
case QColor::Hsl:
185+
mFunc = _interpolateHsl;
186+
break;
187+
}
188+
}
189+
47190
QgsGradientColorRamp::QgsGradientColorRamp( const QColor &color1, const QColor &color2,
48191
bool discrete, const QgsGradientStopsList &stops )
49192
: mColor1( color1 )
50193
, mColor2( color2 )
51194
, mDiscrete( discrete )
52195
, mStops( stops )
196+
, mFunc( _interpolateRgb )
53197
{
54198
}
55199

@@ -67,19 +211,30 @@ QgsColorRamp *QgsGradientColorRamp::create( const QVariantMap &props )
67211
QgsGradientStopsList stops;
68212
if ( props.contains( QStringLiteral( "stops" ) ) )
69213
{
70-
const auto constSplit = props["stops"].toString().split( ':' );
214+
const auto constSplit = props[QStringLiteral( "stops" )].toString().split( ':' );
71215
for ( const QString &stop : constSplit )
72216
{
73-
int i = stop.indexOf( ';' );
74-
if ( i == -1 )
217+
const QStringList parts = stop.split( ';' );
218+
if ( parts.size() != 2 && parts.size() != 4 )
75219
continue;
76220

77-
QColor c = QgsSymbolLayerUtils::decodeColor( stop.mid( i + 1 ) );
78-
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
79-
stops.append( QgsGradientStop( stop.leftRef( i ).toDouble(), c ) );
80-
#else
81-
stops.append( QgsGradientStop( QStringView {stop}.left( i ).toDouble(), c ) );
82-
#endif
221+
QColor c = QgsSymbolLayerUtils::decodeColor( parts.at( 1 ) );
222+
stops.append( QgsGradientStop( parts.at( 0 ).toDouble(), c ) );
223+
224+
if ( parts.size() == 4 )
225+
{
226+
if ( parts.at( 2 ).compare( QLatin1String( "rgb" ) ) == 0 )
227+
stops.last().setColorSpec( QColor::Spec::Rgb );
228+
else if ( parts.at( 2 ).compare( QLatin1String( "hsv" ) ) == 0 )
229+
stops.last().setColorSpec( QColor::Spec::Hsv );
230+
else if ( parts.at( 2 ).compare( QLatin1String( "hsl" ) ) == 0 )
231+
stops.last().setColorSpec( QColor::Spec::Hsl );
232+
233+
if ( parts.at( 3 ).compare( QLatin1String( "cw" ) ) == 0 )
234+
stops.last().setDirection( Qgis::AngularDirection::Clockwise );
235+
else if ( parts.at( 3 ).compare( QLatin1String( "ccw" ) ) == 0 )
236+
stops.last().setDirection( Qgis::AngularDirection::CounterClockwise );
237+
}
83238
}
84239
}
85240

@@ -102,6 +257,27 @@ QgsColorRamp *QgsGradientColorRamp::create( const QVariantMap &props )
102257

103258
QgsGradientColorRamp *r = new QgsGradientColorRamp( color1, color2, discrete, stops );
104259
r->setInfo( info );
260+
261+
if ( props.contains( QStringLiteral( "spec" ) ) )
262+
{
263+
const QString spec = props.value( QStringLiteral( "spec" ) ).toString().trimmed();
264+
if ( spec.compare( QLatin1String( "rgb" ) ) == 0 )
265+
r->setColorSpec( QColor::Spec::Rgb );
266+
else if ( spec.compare( QLatin1String( "hsv" ) ) == 0 )
267+
r->setColorSpec( QColor::Spec::Hsv );
268+
else if ( spec.compare( QLatin1String( "hsl" ) ) == 0 )
269+
r->setColorSpec( QColor::Spec::Hsl );
270+
}
271+
272+
if ( props.contains( QStringLiteral( "direction" ) ) )
273+
{
274+
const QString direction = props.value( QStringLiteral( "direction" ) ).toString().trimmed();
275+
if ( direction.compare( QLatin1String( "ccw" ) ) == 0 )
276+
r->setDirection( Qgis::AngularDirection::CounterClockwise );
277+
else if ( direction.compare( QLatin1String( "cw" ) ) == 0 )
278+
r->setDirection( Qgis::AngularDirection::Clockwise );
279+
}
280+
105281
return r;
106282
}
107283

@@ -136,7 +312,7 @@ QColor QgsGradientColorRamp::color( double value ) const
136312
if ( mDiscrete )
137313
return mColor1;
138314

139-
return _interpolate( mColor1, mColor2, value );
315+
return mFunc( mColor1, mColor2, value, mDirection );
140316
}
141317
else
142318
{
@@ -152,7 +328,7 @@ QColor QgsGradientColorRamp::color( double value ) const
152328
upper = it->offset;
153329
c2 = it->color;
154330

155-
return qgsDoubleNear( upper, lower ) ? c1 : _interpolate( c1, c2, ( value - lower ) / ( upper - lower ) );
331+
return qgsDoubleNear( upper, lower ) ? c1 : it->mFunc( c1, c2, ( value - lower ) / ( upper - lower ), it->mDirection );
156332
}
157333
lower = it->offset;
158334
c1 = it->color;
@@ -163,7 +339,7 @@ QColor QgsGradientColorRamp::color( double value ) const
163339

164340
upper = 1;
165341
c2 = mColor2;
166-
return qgsDoubleNear( upper, lower ) ? c1 : _interpolate( c1, c2, ( value - lower ) / ( upper - lower ) );
342+
return qgsDoubleNear( upper, lower ) ? c1 : mFunc( c1, c2, ( value - lower ) / ( upper - lower ), mDirection );
167343
}
168344
}
169345

@@ -204,6 +380,8 @@ QgsGradientColorRamp *QgsGradientColorRamp::clone() const
204380
QgsGradientColorRamp *r = new QgsGradientColorRamp( mColor1, mColor2,
205381
mDiscrete, mStops );
206382
r->setInfo( mInfo );
383+
r->setColorSpec( mColorSpec );
384+
r->setDirection( mDirection );
207385
return r;
208386
}
209387

@@ -215,9 +393,14 @@ QVariantMap QgsGradientColorRamp::properties() const
215393
if ( !mStops.isEmpty() )
216394
{
217395
QStringList lst;
218-
for ( QgsGradientStopsList::const_iterator it = mStops.begin(); it != mStops.end(); ++it )
396+
lst.reserve( mStops.size() );
397+
for ( const QgsGradientStop &stop : mStops )
219398
{
220-
lst.append( QStringLiteral( "%1;%2" ).arg( it->offset ).arg( QgsSymbolLayerUtils::encodeColor( it->color ) ) );
399+
lst.append( QStringLiteral( "%1;%2;%3;%4" ).arg( stop.offset ).arg( QgsSymbolLayerUtils::encodeColor( stop.color ),
400+
stop.colorSpec() == QColor::Rgb ? QStringLiteral( "rgb" )
401+
: stop.colorSpec() == QColor::Hsv ? QStringLiteral( "hsv" )
402+
: stop.colorSpec() == QColor::Hsl ? QStringLiteral( "hsl" ) : QString(),
403+
stop.direction() == Qgis::AngularDirection::CounterClockwise ? QStringLiteral( "ccw" ) : QStringLiteral( "cw" ) ) );
221404
}
222405
map[QStringLiteral( "stops" )] = lst.join( QLatin1Char( ':' ) );
223406
}
@@ -230,6 +413,33 @@ QVariantMap QgsGradientColorRamp::properties() const
230413
map["info_" + it.key()] = it.value();
231414
}
232415

416+
switch ( mColorSpec )
417+
{
418+
case QColor::Rgb:
419+
map[QStringLiteral( "spec" ) ] = QStringLiteral( "rgb" );
420+
break;
421+
case QColor::Hsv:
422+
map[QStringLiteral( "spec" ) ] = QStringLiteral( "hsv" );
423+
break;
424+
case QColor::Hsl:
425+
map[QStringLiteral( "spec" ) ] = QStringLiteral( "hsl" );
426+
break;
427+
case QColor::Cmyk:
428+
case QColor::Invalid:
429+
case QColor::ExtendedRgb:
430+
break;
431+
}
432+
433+
switch ( mDirection )
434+
{
435+
case Qgis::AngularDirection::Clockwise:
436+
map[QStringLiteral( "direction" ) ] = QStringLiteral( "cw" );
437+
break;
438+
case Qgis::AngularDirection::CounterClockwise:
439+
map[QStringLiteral( "direction" ) ] = QStringLiteral( "ccw" );
440+
break;
441+
}
442+
233443
map[QStringLiteral( "rampType" )] = type();
234444
return map;
235445
}
@@ -313,6 +523,26 @@ void QgsGradientColorRamp::addStopsToGradient( QGradient *gradient, double opaci
313523
}
314524
}
315525

526+
void QgsGradientColorRamp::setColorSpec( QColor::Spec spec )
527+
{
528+
mColorSpec = spec;
529+
switch ( mColorSpec )
530+
{
531+
case QColor::Rgb:
532+
case QColor::Invalid:
533+
case QColor::ExtendedRgb:
534+
case QColor::Cmyk:
535+
mFunc = _interpolateRgb;
536+
break;
537+
case QColor::Hsv:
538+
mFunc = _interpolateHsv;
539+
break;
540+
case QColor::Hsl:
541+
mFunc = _interpolateHsl;
542+
break;
543+
}
544+
}
545+
316546

317547
//////////////
318548

@@ -950,4 +1180,3 @@ QgsNamedColorList QgsPresetSchemeColorRamp::fetchColors( const QString &, const
9501180
{
9511181
return mColors;
9521182
}
953-

‎src/core/qgscolorrampimpl.h

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
#include "qgscolorscheme.h"
2424
#include "qgscolorramp.h"
2525

26+
#ifndef SIP_RUN
27+
///@cond PRIVATE
28+
typedef QColor( *InterpolateColorFunc )( const QColor &c1, const QColor &c2, const double value, Qgis::AngularDirection direction );
29+
///@endcond
30+
#endif
31+
2632
/**
2733
* \ingroup core
2834
* \class QgsGradientStop
@@ -38,10 +44,7 @@ class CORE_EXPORT QgsGradientStop
3844
* \param offset positional offset for stop, between 0 and 1.0
3945
* \param color color for stop
4046
*/
41-
QgsGradientStop( double offset, const QColor &color )
42-
: offset( offset )
43-
, color( color )
44-
{ }
47+
QgsGradientStop( double offset, const QColor &color );
4548

4649
//! Relative positional offset, between 0 and 1
4750
double offset;
@@ -50,8 +53,69 @@ class CORE_EXPORT QgsGradientStop
5053

5154
bool operator==( const QgsGradientStop &other ) const
5255
{
53-
return other.color == color && qgsDoubleNear( other.offset, offset );
56+
return other.color == color && qgsDoubleNear( other.offset, offset ) && other.mColorSpec == mColorSpec && other.mDirection == mDirection;
5457
}
58+
59+
bool operator!=( const QgsGradientStop &other ) const
60+
{
61+
return !( *this == other );
62+
}
63+
64+
/**
65+
* Returns the color specification in which the color component interpolation will occur.
66+
*
67+
* For multi-stop gradients this color spec will be used for the portion of the color ramp
68+
* leading into the current stop.
69+
*
70+
* \see setColorSpec()
71+
* \since QGIS 3.24
72+
*/
73+
QColor::Spec colorSpec() const { return mColorSpec; }
74+
75+
/**
76+
* Sets the color specification in which the color component interpolation will occur.
77+
*
78+
* Only QColor::Spec::Rgb, QColor::Spec::Hsv and QColor::Spec::Hsl are currently supported.
79+
*
80+
* For multi-stop gradients this color spec will be used for the portion of the color ramp
81+
* leading into the current stop.
82+
*
83+
* \see colorSpec()
84+
* \since QGIS 3.24
85+
*/
86+
void setColorSpec( QColor::Spec spec );
87+
88+
/**
89+
* Returns the direction to traverse the color wheel using when interpolating hue-based color
90+
* specifications.
91+
*
92+
* For multi-stop gradients this direction will be used for the portion of the color ramp
93+
* leading into the current stop.
94+
*
95+
* \see setDirection()
96+
* \since QGIS 3.24
97+
*/
98+
Qgis::AngularDirection direction() const { return mDirection; }
99+
100+
/**
101+
* Sets the \a direction to traverse the color wheel using when interpolating hue-based color
102+
* specifications.
103+
*
104+
* For multi-stop gradients this direction will be used for the portion of the color ramp
105+
* leading into the current stop.
106+
*
107+
* \see direction()
108+
* \since QGIS 3.24
109+
*/
110+
void setDirection( Qgis::AngularDirection direction ) { mDirection = direction; }
111+
112+
private:
113+
114+
QColor::Spec mColorSpec = QColor::Spec::Rgb;
115+
Qgis::AngularDirection mDirection = Qgis::AngularDirection::CounterClockwise;
116+
InterpolateColorFunc mFunc = nullptr;
117+
118+
friend class QgsGradientColorRamp;
55119
};
56120

57121
//! List of gradient stops
@@ -197,12 +261,64 @@ class CORE_EXPORT QgsGradientColorRamp : public QgsColorRamp
197261
*/
198262
void addStopsToGradient( QGradient *gradient, double opacity = 1 );
199263

264+
/**
265+
* Returns the color specification in which the color component interpolation will occur.
266+
*
267+
* For multi-stop gradients this color spec will be used for the portion of the color ramp
268+
* leading into the final stop (i.e. color2()).
269+
*
270+
* \see setColorSpec()
271+
* \since QGIS 3.24
272+
*/
273+
QColor::Spec colorSpec() const { return mColorSpec; }
274+
275+
/**
276+
* Sets the color specification in which the color component interpolation will occur.
277+
*
278+
* Only QColor::Spec::Rgb, QColor::Spec::Hsv and QColor::Spec::Hsl are currently supported.
279+
*
280+
* For multi-stop gradients this color spec will be used for the portion of the color ramp
281+
* leading into the final stop (i.e. color2()).
282+
*
283+
* \see colorSpec()
284+
* \since QGIS 3.24
285+
*/
286+
void setColorSpec( QColor::Spec spec );
287+
288+
/**
289+
* Returns the direction to traverse the color wheel using when interpolating hue-based color
290+
* specifications.
291+
*
292+
* For multi-stop gradients this direction will be used for the portion of the color ramp
293+
* leading into the final stop (i.e. color2()).
294+
*
295+
* \see setDirection()
296+
* \since QGIS 3.24
297+
*/
298+
Qgis::AngularDirection direction() const { return mDirection; }
299+
300+
/**
301+
* Sets the \a direction to traverse the color wheel using when interpolating hue-based color
302+
* specifications.
303+
*
304+
* For multi-stop gradients this direction will be used for the portion of the color ramp
305+
* leading into the final stop (i.e. color2()).
306+
*
307+
* \see direction()
308+
* \since QGIS 3.24
309+
*/
310+
void setDirection( Qgis::AngularDirection direction ) { mDirection = direction; }
311+
200312
protected:
201313
QColor mColor1;
202314
QColor mColor2;
203315
bool mDiscrete;
204316
QgsGradientStopsList mStops;
205317
QgsStringMap mInfo;
318+
QColor::Spec mColorSpec = QColor::Spec::Rgb;
319+
Qgis::AngularDirection mDirection = Qgis::AngularDirection::CounterClockwise;
320+
321+
InterpolateColorFunc mFunc = nullptr;
206322
};
207323

208324
Q_DECLARE_METATYPE( QgsGradientColorRamp )

‎src/gui/qgsgradientcolorrampdialog.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,35 @@ QgsGradientColorRampDialog::QgsGradientColorRampDialog( const QgsGradientColorRa
5252
{
5353
setupUi( this );
5454
QgsGui::instance()->enableAutoGeometryRestore( this );
55+
56+
mStopColorSpec->addItem( tr( "RGB" ), static_cast< int >( QColor::Spec::Rgb ) );
57+
mStopColorSpec->addItem( tr( "HSV" ), static_cast< int >( QColor::Spec::Hsv ) );
58+
mStopColorSpec->addItem( tr( "HSL" ), static_cast< int >( QColor::Spec::Hsl ) );
59+
mStopColorSpec->setCurrentIndex( mStopColorSpec->findData( static_cast< int >( ramp.colorSpec() ) ) );
60+
61+
mStopDirection->addItem( tr( "Clockwise" ), static_cast< int >( Qgis::AngularDirection::Clockwise ) );
62+
mStopDirection->addItem( tr( "Counterclockwise" ), static_cast< int >( Qgis::AngularDirection::CounterClockwise ) );
63+
mStopDirection->setCurrentIndex( mStopColorSpec->findData( static_cast< int >( ramp.direction() ) ) );
64+
65+
mStopDirection->setEnabled( static_cast< QColor::Spec>( mStopColorSpec->currentData().toInt() ) != QColor::Spec::Rgb );
66+
67+
connect( mStopColorSpec, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [ = ]
68+
{
69+
mStopDirection->setEnabled( static_cast< QColor::Spec>( mStopColorSpec->currentData().toInt() ) != QColor::Spec::Rgb );
70+
71+
if ( mBlockChanges )
72+
return;
73+
mStopEditor->setSelectedStopColorSpec( static_cast< QColor::Spec>( mStopColorSpec->currentData().toInt() ) );
74+
} );
75+
76+
connect( mStopDirection, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [ = ]
77+
{
78+
if ( mBlockChanges )
79+
return;
80+
81+
mStopEditor->setSelectedStopDirection( static_cast< Qgis::AngularDirection >( mStopDirection->currentData().toInt() ) );
82+
} );
83+
5584
connect( cboType, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsGradientColorRampDialog::cboType_currentIndexChanged );
5685
connect( btnInformation, &QPushButton::pressed, this, &QgsGradientColorRampDialog::btnInformation_pressed );
5786
connect( mPositionSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsGradientColorRampDialog::mPositionSpinBox_valueChanged );
@@ -304,13 +333,18 @@ void QgsGradientColorRampDialog::updateStopEditor()
304333

305334
void QgsGradientColorRampDialog::selectedStopChanged( const QgsGradientStop &stop )
306335
{
336+
mBlockChanges++;
307337
mColorWidget->blockSignals( true );
308338
mColorWidget->setColor( stop.color );
309339
mColorWidget->blockSignals( false );
310340
mPositionSpinBox->blockSignals( true );
311341
mPositionSpinBox->setValue( stop.offset * 100 );
312342
mPositionSpinBox->blockSignals( false );
313343

344+
mStopColorSpec->setCurrentIndex( mStopColorSpec->findData( static_cast< int >( mStopEditor->selectedStop().colorSpec() ) ) );
345+
mStopDirection->setCurrentIndex( mStopDirection->findData( static_cast< int >( mStopEditor->selectedStop().direction() ) ) );
346+
mBlockChanges--;
347+
314348
if ( ( stop.offset == 0 && stop.color == mRamp.color1() ) || ( stop.offset == 1.0 && stop.color == mRamp.color2() ) )
315349
{
316350
//first/last stop can't be repositioned
@@ -323,6 +357,10 @@ void QgsGradientColorRampDialog::selectedStopChanged( const QgsGradientStop &sto
323357
mDeleteStopButton->setDisabled( false );
324358
}
325359

360+
// first stop cannot have color spec or direction set
361+
mStopColorSpec->setEnabled( !( stop.offset == 0 && stop.color == mRamp.color1() ) );
362+
mStopDirection->setEnabled( !( stop.offset == 0 && stop.color == mRamp.color1() ) && mStopEditor->selectedStop().colorSpec() != QColor::Rgb );
363+
326364
updatePlot();
327365
}
328366

@@ -556,13 +594,23 @@ void QgsGradientColorRampDialog::updatePlot()
556594
void QgsGradientColorRampDialog::updateRampFromStopEditor()
557595
{
558596
mRamp = mStopEditor->gradientRamp();
597+
598+
mBlockChanges++;
559599
mPositionSpinBox->blockSignals( true );
560600
mPositionSpinBox->setValue( mStopEditor->selectedStop().offset * 100 );
561601
mPositionSpinBox->blockSignals( false );
562602
mColorWidget->blockSignals( true );
563603
mColorWidget->setColor( mStopEditor->selectedStop().color );
564604
mColorWidget->blockSignals( false );
565605

606+
mStopColorSpec->setCurrentIndex( mStopColorSpec->findData( static_cast< int >( mStopEditor->selectedStop().colorSpec() ) ) );
607+
mStopDirection->setCurrentIndex( mStopDirection->findData( static_cast< int >( mStopEditor->selectedStop().direction() ) ) );
608+
mBlockChanges--;
609+
610+
// first stop cannot have color spec or direction set
611+
mStopColorSpec->setEnabled( !( mStopEditor->selectedStop().offset == 0 && mStopEditor->selectedStop().color == mRamp.color1() ) );
612+
mStopDirection->setEnabled( !( mStopEditor->selectedStop().offset == 0 && mStopEditor->selectedStop().color == mRamp.color1() ) && mStopEditor->selectedStop().colorSpec() != QColor::Rgb );
613+
566614
updateColorButtons();
567615
updatePlot();
568616

‎src/gui/qgsgradientcolorrampdialog.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class GUI_EXPORT QgsGradientColorRampDialog : public QDialog, private Ui::QgsGra
117117
QgsGradientPlotEventFilter *mPlotFilter = nullptr;
118118
int mCurrentPlotColorComponent;
119119
int mCurrentPlotMarkerIndex;
120+
int mBlockChanges = 0;
120121

121122
void updatePlot();
122123
void addPlotMarker( double x, double y, const QColor &color, bool isSelected = false );

‎src/gui/qgsgradientstopeditor.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ QgsGradientStop QgsGradientStopEditor::selectedStop() const
172172
}
173173
else
174174
{
175-
return QgsGradientStop( 1.0, mGradient.color2() );
175+
QgsGradientStop stop( 1.0, mGradient.color2() );
176+
stop.setColorSpec( mGradient.colorSpec() );
177+
stop.setDirection( mGradient.direction() );
178+
return stop;
176179
}
177180
}
178181

@@ -206,6 +209,40 @@ void QgsGradientStopEditor::setSelectedStopOffset( double offset )
206209
}
207210
}
208211

212+
void QgsGradientStopEditor::setSelectedStopColorSpec( QColor::Spec spec )
213+
{
214+
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
215+
{
216+
mStops[ mSelectedStop - 1 ].setColorSpec( spec );
217+
mGradient.setStops( mStops );
218+
update();
219+
emit changed();
220+
}
221+
else if ( mSelectedStop == mGradient.count() - 1 )
222+
{
223+
mGradient.setColorSpec( spec );
224+
update();
225+
emit changed();
226+
}
227+
}
228+
229+
void QgsGradientStopEditor::setSelectedStopDirection( Qgis::AngularDirection direction )
230+
{
231+
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
232+
{
233+
mStops[ mSelectedStop - 1 ].setDirection( direction );
234+
mGradient.setStops( mStops );
235+
update();
236+
emit changed();
237+
}
238+
else if ( mSelectedStop == mGradient.count() - 1 )
239+
{
240+
mGradient.setDirection( direction );
241+
update();
242+
emit changed();
243+
}
244+
}
245+
209246
void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
210247
{
211248
if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )

‎src/gui/qgsgradientstopeditor.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ class GUI_EXPORT QgsGradientStopEditor : public QWidget
9393
*/
9494
void setSelectedStopOffset( double offset );
9595

96+
/**
97+
* Sets the color \a spec for the current selected stop.
98+
*
99+
* \since QGIS 3.24
100+
*/
101+
void setSelectedStopColorSpec( QColor::Spec spec );
102+
103+
/**
104+
* Sets the hue angular direction for the current selected stop.
105+
*
106+
* \since QGIS 3.24
107+
*/
108+
void setSelectedStopDirection( Qgis::AngularDirection direction );
109+
96110
/**
97111
* Sets the color and offset for the current selected stop.
98112
* \param color new stop color

‎src/ui/qgsgradientcolorrampdialogbase.ui

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
<x>0</x>
149149
<y>0</y>
150150
<width>850</width>
151-
<height>498</height>
151+
<height>494</height>
152152
</rect>
153153
</property>
154154
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
@@ -180,6 +180,20 @@
180180
</property>
181181
</widget>
182182
</item>
183+
<item row="0" column="5">
184+
<widget class="QPushButton" name="mDeleteStopButton">
185+
<property name="text">
186+
<string>&amp;Delete Stop</string>
187+
</property>
188+
</widget>
189+
</item>
190+
<item row="1" column="0" colspan="5">
191+
<widget class="QgsCompoundColorWidget" name="mColorWidget" native="true">
192+
<property name="focusPolicy">
193+
<enum>Qt::StrongFocus</enum>
194+
</property>
195+
</widget>
196+
</item>
183197
<item row="0" column="1">
184198
<widget class="QgsDoubleSpinBox" name="mPositionSpinBox">
185199
<property name="suffix">
@@ -190,14 +204,7 @@
190204
</property>
191205
</widget>
192206
</item>
193-
<item row="0" column="2">
194-
<widget class="QPushButton" name="mDeleteStopButton">
195-
<property name="text">
196-
<string>&amp;Delete Stop</string>
197-
</property>
198-
</widget>
199-
</item>
200-
<item row="0" column="3">
207+
<item row="0" column="4">
201208
<spacer name="horizontalSpacer">
202209
<property name="orientation">
203210
<enum>Qt::Horizontal</enum>
@@ -210,12 +217,11 @@
210217
</property>
211218
</spacer>
212219
</item>
213-
<item row="1" column="0" colspan="4">
214-
<widget class="QgsCompoundColorWidget" name="mColorWidget" native="true">
215-
<property name="focusPolicy">
216-
<enum>Qt::StrongFocus</enum>
217-
</property>
218-
</widget>
220+
<item row="0" column="2">
221+
<widget class="QComboBox" name="mStopColorSpec"/>
222+
</item>
223+
<item row="0" column="3">
224+
<widget class="QComboBox" name="mStopDirection"/>
219225
</item>
220226
</layout>
221227
</widget>
@@ -331,9 +337,9 @@
331337
</widget>
332338
<customwidgets>
333339
<customwidget>
334-
<class>QwtPlot</class>
335-
<extends>QFrame</extends>
336-
<header>qwt_plot.h</header>
340+
<class>QgsScrollArea</class>
341+
<extends>QScrollArea</extends>
342+
<header>qgsscrollarea.h</header>
337343
<container>1</container>
338344
</customwidget>
339345
<customwidget>
@@ -347,18 +353,18 @@
347353
<extends>QDoubleSpinBox</extends>
348354
<header>qgsdoublespinbox.h</header>
349355
</customwidget>
350-
<customwidget>
351-
<class>QgsScrollArea</class>
352-
<extends>QScrollArea</extends>
353-
<header>qgsscrollarea.h</header>
354-
<container>1</container>
355-
</customwidget>
356356
<customwidget>
357357
<class>QgsColorButton</class>
358358
<extends>QToolButton</extends>
359359
<header>qgscolorbutton.h</header>
360360
<container>1</container>
361361
</customwidget>
362+
<customwidget>
363+
<class>QwtPlot</class>
364+
<extends>QFrame</extends>
365+
<header>qwt_plot.h</header>
366+
<container>1</container>
367+
</customwidget>
362368
<customwidget>
363369
<class>QgsGradientStopEditor</class>
364370
<extends>QWidget</extends>
@@ -379,6 +385,8 @@
379385
<tabstop>mStopEditor</tabstop>
380386
<tabstop>scrollArea</tabstop>
381387
<tabstop>mPositionSpinBox</tabstop>
388+
<tabstop>mStopColorSpec</tabstop>
389+
<tabstop>mStopDirection</tabstop>
382390
<tabstop>mDeleteStopButton</tabstop>
383391
<tabstop>mColorWidget</tabstop>
384392
<tabstop>mPlotHueCheckbox</tabstop>

‎tests/src/python/test_qgscolorramp.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212

1313
import qgis # NOQA
1414

15-
from qgis.core import (QgsGradientColorRamp,
15+
from qgis.core import (Qgis,
16+
QgsGradientColorRamp,
1617
QgsGradientStop,
1718
QgsLimitedRandomColorRamp,
1819
QgsRandomColorRamp,
@@ -34,6 +35,16 @@ def testQgsGradientColorRamp(self):
3435
self.assertNotEqual(QgsGradientStop(0.1, QColor(180, 20, 30)), QgsGradientStop(0.2, QColor(180, 20, 30)))
3536
self.assertNotEqual(QgsGradientStop(0.1, QColor(180, 20, 30)), QgsGradientStop(0.1, QColor(180, 40, 30)))
3637

38+
stop2 = QgsGradientStop(stop)
39+
stop2.setColorSpec(QColor.Hsv)
40+
self.assertNotEqual(stop2, stop)
41+
self.assertEqual(stop2.colorSpec(), QColor.Hsv)
42+
43+
stop2 = QgsGradientStop(stop)
44+
stop2.setDirection(Qgis.AngularDirection.Clockwise)
45+
self.assertNotEqual(stop2, stop)
46+
self.assertEqual(stop2.direction(), Qgis.AngularDirection.Clockwise)
47+
3748
# test gradient with only start/end color
3849
r = QgsGradientColorRamp(QColor(200, 0, 0, 100), QColor(0, 200, 0, 200))
3950
self.assertEqual(r.type(), 'gradient')
@@ -48,6 +59,85 @@ def testQgsGradientColorRamp(self):
4859
self.assertEqual(r.color(1), QColor(0, 200, 0, 200))
4960
self.assertEqual(r.color(0.5), QColor(100, 100, 0, 150))
5061

62+
r.setColorSpec(QColor.Hsv)
63+
self.assertEqual(r.colorSpec(), QColor.Hsv)
64+
r.setColor1(QColor.fromHsvF(0.1, 0.2, 0.4, 0.5))
65+
r.setColor2(QColor.fromHsvF(0.3, 0.4, 0.6, 0.7))
66+
self.assertAlmostEqual(r.color(0).hsvHueF(), 0.1, 3)
67+
self.assertAlmostEqual(r.color(0).hsvSaturationF(), 0.2, 3)
68+
self.assertAlmostEqual(r.color(0).valueF(), 0.4, 3)
69+
self.assertAlmostEqual(r.color(0).alphaF(), 0.5, 3)
70+
71+
self.assertAlmostEqual(r.color(1).hsvHueF(), 0.3, 3)
72+
self.assertAlmostEqual(r.color(1).hsvSaturationF(), 0.4, 3)
73+
self.assertAlmostEqual(r.color(1).valueF(), 0.6, 3)
74+
self.assertAlmostEqual(r.color(1).alphaF(), 0.7, 3)
75+
76+
self.assertAlmostEqual(r.color(0.5).hsvHueF(), 0.2, 3)
77+
self.assertAlmostEqual(r.color(0.5).hsvSaturationF(), 0.3, 3)
78+
self.assertAlmostEqual(r.color(0.5).valueF(), 0.5, 3)
79+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
80+
81+
r.setDirection(Qgis.AngularDirection.Clockwise)
82+
self.assertAlmostEqual(r.color(0.5).hsvHueF(), 0.7, 3)
83+
self.assertAlmostEqual(r.color(0.5).hsvSaturationF(), 0.3, 3)
84+
self.assertAlmostEqual(r.color(0.5).valueF(), 0.5, 3)
85+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
86+
87+
r.setDirection(Qgis.AngularDirection.CounterClockwise)
88+
r.setColor1(QColor.fromHsvF(0.1, 0.2, 0.4, 0.5))
89+
r.setColor2(QColor.fromHsvF(0.3, 0.4, 0.6, 0.7))
90+
self.assertAlmostEqual(r.color(0.5).hsvHueF(), 0.2, 3)
91+
self.assertAlmostEqual(r.color(0.5).hsvSaturationF(), 0.3, 3)
92+
self.assertAlmostEqual(r.color(0.5).valueF(), 0.5, 3)
93+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
94+
95+
r.setDirection(Qgis.AngularDirection.Clockwise)
96+
self.assertAlmostEqual(r.color(0.5).hsvHueF(), 0.7, 3)
97+
self.assertAlmostEqual(r.color(0.5).hsvSaturationF(), 0.3, 3)
98+
self.assertAlmostEqual(r.color(0.5).valueF(), 0.5, 3)
99+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
100+
101+
r.setColorSpec(QColor.Hsl)
102+
r.setDirection(Qgis.AngularDirection.CounterClockwise)
103+
self.assertEqual(r.colorSpec(), QColor.Hsl)
104+
r.setColor1(QColor.fromHslF(0.1, 0.2, 0.4, 0.5))
105+
r.setColor2(QColor.fromHslF(0.3, 0.4, 0.6, 0.7))
106+
self.assertAlmostEqual(r.color(0).hslHueF(), 0.1, 3)
107+
self.assertAlmostEqual(r.color(0).hslSaturationF(), 0.2, 3)
108+
self.assertAlmostEqual(r.color(0).lightnessF(), 0.4, 3)
109+
self.assertAlmostEqual(r.color(0).alphaF(), 0.5, 3)
110+
111+
self.assertAlmostEqual(r.color(1).hslHueF(), 0.3, 3)
112+
self.assertAlmostEqual(r.color(1).hslSaturationF(), 0.4, 3)
113+
self.assertAlmostEqual(r.color(1).lightnessF(), 0.6, 3)
114+
self.assertAlmostEqual(r.color(1).alphaF(), 0.7, 3)
115+
116+
self.assertAlmostEqual(r.color(0.5).hslHueF(), 0.2, 3)
117+
self.assertAlmostEqual(r.color(0.5).hslSaturationF(), 0.3, 3)
118+
self.assertAlmostEqual(r.color(0.5).lightnessF(), 0.5, 3)
119+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
120+
121+
r.setDirection(Qgis.AngularDirection.Clockwise)
122+
self.assertAlmostEqual(r.color(0.5).hslHueF(), 0.7, 3)
123+
self.assertAlmostEqual(r.color(0.5).hslSaturationF(), 0.3, 3)
124+
self.assertAlmostEqual(r.color(0.5).lightnessF(), 0.5, 3)
125+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
126+
127+
r.setDirection(Qgis.AngularDirection.CounterClockwise)
128+
r.setColor1(QColor.fromHslF(0.1, 0.2, 0.4, 0.5))
129+
r.setColor2(QColor.fromHslF(0.3, 0.4, 0.6, 0.7))
130+
self.assertAlmostEqual(r.color(0.5).hslHueF(), 0.2, 3)
131+
self.assertAlmostEqual(r.color(0.5).hslSaturationF(), 0.3, 3)
132+
self.assertAlmostEqual(r.color(0.5).lightnessF(), 0.5, 3)
133+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
134+
135+
r.setDirection(Qgis.AngularDirection.Clockwise)
136+
self.assertAlmostEqual(r.color(0.5).hslHueF(), 0.7, 3)
137+
self.assertAlmostEqual(r.color(0.5).hslSaturationF(), 0.3, 3)
138+
self.assertAlmostEqual(r.color(0.5).lightnessF(), 0.5, 3)
139+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.6, 3)
140+
51141
# test gradient with stops
52142
r = QgsGradientColorRamp(QColor(200, 0, 0), QColor(0, 200, 0), False, [QgsGradientStop(0.1, QColor(180, 20, 40)),
53143
QgsGradientStop(0.9, QColor(40, 60, 100))])
@@ -68,14 +158,60 @@ def testQgsGradientColorRamp(self):
68158
self.assertEqual(r.color(0.95), QColor(20, 130, 50))
69159
self.assertEqual(r.color(1), QColor(0, 200, 0))
70160

161+
# with color models
162+
r = QgsGradientColorRamp(QColor.fromHsvF(0.2, 0.4, 0.6, 0.3), QColor(0, 200, 0))
163+
stop1 = QgsGradientStop(0.1, QColor.fromHsvF(0.4, 0.6, 0.8, 0.1))
164+
stop1.setColorSpec(QColor.Hsv)
165+
stop2 = QgsGradientStop(0.5, QColor.fromHslF(0.3, 0.2, 0.1, 0.9))
166+
stop2.setColorSpec(QColor.Hsl)
167+
stop3 = QgsGradientStop(0.9, QColor(60, 100, 120))
168+
stop3.setColorSpec(QColor.Rgb)
169+
r.setStops([stop1, stop2, stop3])
170+
self.assertAlmostEqual(r.color(0).hsvHueF(), 0.2, 3)
171+
self.assertAlmostEqual(r.color(0).hsvSaturationF(), 0.4, 3)
172+
self.assertAlmostEqual(r.color(0).valueF(), 0.6, 3)
173+
self.assertAlmostEqual(r.color(0).alphaF(), 0.3, 3)
174+
self.assertAlmostEqual(r.color(0.05).hsvHueF(), 0.3, 3)
175+
self.assertAlmostEqual(r.color(0.05).hsvSaturationF(), 0.5, 3)
176+
self.assertAlmostEqual(r.color(0.05).valueF(), 0.7, 3)
177+
self.assertAlmostEqual(r.color(0.05).alphaF(), 0.2, 3)
178+
self.assertAlmostEqual(r.color(0.1).hsvHueF(), 0.4, 3)
179+
self.assertAlmostEqual(r.color(0.1).hsvSaturationF(), 0.6, 3)
180+
self.assertAlmostEqual(r.color(0.1).valueF(), 0.8, 3)
181+
self.assertAlmostEqual(r.color(0.1).alphaF(), 0.1, 3)
182+
self.assertAlmostEqual(r.color(0.1).hslHueF(), 0.4, 3)
183+
self.assertAlmostEqual(r.color(0.1).hslSaturationF(), 0.5454, 3)
184+
self.assertAlmostEqual(r.color(0.1).lightnessF(), 0.56, 3)
185+
self.assertAlmostEqual(r.color(0.3).hslHueF(), 0.85, 3)
186+
self.assertAlmostEqual(r.color(0.3).hslSaturationF(), 0.3727, 3)
187+
self.assertAlmostEqual(r.color(0.3).lightnessF(), 0.330, 3)
188+
self.assertAlmostEqual(r.color(0.3).alphaF(), 0.5, 3)
189+
self.assertAlmostEqual(r.color(0.5).hslHueF(), 0.3, 3)
190+
self.assertAlmostEqual(r.color(0.5).hslSaturationF(), 0.2, 3)
191+
self.assertAlmostEqual(r.color(0.5).lightnessF(), 0.1, 3)
192+
self.assertAlmostEqual(r.color(0.5).alphaF(), 0.9, 3)
193+
self.assertEqual(r.color(0.5).red(), 22)
194+
self.assertEqual(r.color(0.5).green(), 31)
195+
self.assertEqual(r.color(0.5).blue(), 20)
196+
self.assertEqual(r.color(0.7).red(), 41)
197+
self.assertEqual(r.color(0.7).green(), 65)
198+
self.assertEqual(r.color(0.7).blue(), 70)
199+
self.assertAlmostEqual(r.color(0.7).alphaF(), 0.95, 3)
200+
self.assertEqual(r.color(0.9), QColor(60, 100, 120))
201+
self.assertEqual(r.color(0.95), QColor(30, 150, 60))
202+
self.assertEqual(r.color(1), QColor(0, 200, 0))
203+
71204
# test setters
72205
r.setColor1(QColor(0, 0, 200))
73206
self.assertEqual(r.color1(), QColor(0, 0, 200))
74207
self.assertEqual(r.color(0), QColor(0, 0, 200))
75208
r.setColor2(QColor(0, 0, 100))
76209
self.assertEqual(r.color2(), QColor(0, 0, 100))
77210
self.assertEqual(r.color(1.0), QColor(0, 0, 100))
78-
r.setStops([QgsGradientStop(0.4, QColor(100, 100, 40))])
211+
stop = QgsGradientStop(0.4, QColor(100, 100, 40))
212+
stop.setColorSpec(QColor.Hsv)
213+
stop.setDirection(Qgis.AngularDirection.Clockwise)
214+
r.setStops([stop])
79215
s = r.stops()
80216
self.assertEqual(len(s), 1)
81217
self.assertEqual(s[0].offset, 0.4)
@@ -94,6 +230,8 @@ def testQgsGradientColorRamp(self):
94230
s = fromProps.stops()
95231
self.assertEqual(len(s), 1)
96232
self.assertEqual(s[0].offset, 0.4)
233+
self.assertEqual(s[0].colorSpec(), QColor.Hsv)
234+
self.assertEqual(s[0].direction(), Qgis.AngularDirection.Clockwise)
97235
c = QColor(s[0].color)
98236
self.assertEqual(c, QColor(100, 100, 40))
99237
self.assertEqual(fromProps.info()['key1'], 'val1')
@@ -107,6 +245,8 @@ def testQgsGradientColorRamp(self):
107245
s = cloned.stops()
108246
self.assertEqual(len(s), 1)
109247
self.assertEqual(s[0].offset, 0.4)
248+
self.assertEqual(s[0].colorSpec(), QColor.Hsv)
249+
self.assertEqual(s[0].direction(), Qgis.AngularDirection.Clockwise)
110250
c = QColor(s[0].color)
111251
self.assertEqual(c, QColor(100, 100, 40))
112252
self.assertEqual(cloned.info()['key1'], 'val1')

0 commit comments

Comments
 (0)
Please sign in to comment.