QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskGradient.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskGradient.h"
7#include "QskRgbValue.h"
8#include "QskGradientDirection.h"
9#include "QskFunctions.h"
10
11#include <qvariant.h>
12#include <qdebug.h>
13
14static void qskRegisterGradient()
15{
16 qRegisterMetaType< QskGradient >();
17
18#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
19 QMetaType::registerEqualsComparator< QskGradient >();
20#endif
21
22 QMetaType::registerConverter< QColor, QskGradient >(
23 []( const QColor& color ) { return QskGradient( color ); } );
24}
25
26Q_CONSTRUCTOR_FUNCTION( qskRegisterGradient )
27
28static inline bool qskIsGradientValid( const QskGradientStops& stops )
29{
30 if ( stops.isEmpty() )
31 return false;
32
33 if ( stops.first().position() < 0.0 || stops.last().position() > 1.0 )
34 return false;
35
36 if ( !stops.first().color().isValid() )
37 return false;
38
39 for ( int i = 1; i < stops.size(); i++ )
40 {
41 if ( stops[ i ].position() < stops[ i - 1 ].position() )
42 return false;
43
44 if ( !stops[ i ].color().isValid() )
45 return false;
46 }
47
48 return true;
49}
50
51static inline bool qskCanBeInterpolated( const QskGradient& from, const QskGradient& to )
52{
53 if ( from.isMonochrome() || to.isMonochrome() )
54 return true;
55
56 return from.type() == to.type();
57}
58
59static inline QTransform qskTransformForRect( int, const QRectF& rect )
60{
61 const qreal x = rect.x();
62 const qreal y = rect.y();
63 const qreal w = rect.width();
64 const qreal h = rect.height();
65
66 return QTransform( w, 0, 0, h, x, y );
67}
68
69QskGradient::QskGradient( const QColor& color )
70 : QskGradient()
71{
72 setStops( color );
73}
74
75QskGradient::QskGradient( const QColor& color1, const QColor& color2 )
76 : QskGradient()
77{
78 setStops( color1, color2 );
79}
80
81QskGradient::QskGradient( QGradient::Preset preset )
82 : QskGradient()
83{
84 setStops( qskBuildGradientStops( QGradient( preset ).stops() ) );
85}
86
87QskGradient::QskGradient( const QVector< QskGradientStop >& stops )
88 : QskGradient()
89{
90 setStops( stops );
91}
92
93QskGradient::QskGradient( const QGradient& qGradient )
94 : QskGradient()
95{
96 switch( qGradient.type() )
97 {
98 case QGradient::LinearGradient:
99 {
100 m_type = Linear;
101
102 const auto g = static_cast< const QLinearGradient* >( &qGradient );
103
104 m_values[0] = g->start().x();
105 m_values[1] = g->start().y();
106 m_values[2] = g->finalStop().x();
107 m_values[3] = g->finalStop().y();
108
109 break;
110 }
111 case QGradient::RadialGradient:
112 {
113 m_type = Radial;
114
115 const auto g = static_cast< const QRadialGradient* >( &qGradient );
116
117 if ( ( g->center() != g->focalPoint() ) || ( g->radius() != g->focalRadius() ) )
118 qWarning() << "QskGradient: extended radial gradients are not supported.";
119
120 m_values[0] = g->focalPoint().x();
121 m_values[1] = g->focalPoint().y();
122 m_values[3] = m_values[2] = g->focalRadius();
123
124 break;
125 }
126 case QGradient::ConicalGradient:
127 {
128 m_type = Conic;
129
130 const auto g = static_cast< const QConicalGradient* >( &qGradient );
131
132 m_values[0] = g->center().x();
133 m_values[1] = g->center().y();
134 m_values[2] = g->angle();
135 m_values[3] = 360.0;
136
137 break;
138 }
139 default:
140 {
141 m_type = Stops;
142 break;
143 }
144 }
145
146 m_spreadMode = static_cast< SpreadMode >( qGradient.spread() );
147
148 switch( qGradient.coordinateMode() )
149 {
150 case QGradient::ObjectMode:
151 case QGradient::ObjectBoundingMode:
152
153 m_stretchMode = StretchToSize;
154 break;
155
156 case QGradient::LogicalMode:
157
158 m_stretchMode = NoStretch;
159 break;
160
161 case QGradient::StretchToDeviceMode:
162 {
163 qWarning() << "QskGradient: StretchToDeviceMode is not supportd.";
164 m_stretchMode = NoStretch;
165 }
166 }
167
168 setStops( qskBuildGradientStops( qGradient.stops() ) );
169}
170
171QskGradient::QskGradient( const QskGradient& other ) noexcept
172 : m_stops( other.m_stops )
173 , m_values{ other.m_values[0], other.m_values[1],
174 other.m_values[2], other.m_values[3], other.m_values[4] }
175 , m_type( other.m_type )
176 , m_spreadMode( other.m_spreadMode )
177 , m_stretchMode( other.m_stretchMode )
178 , m_isDirty( other.m_isDirty )
179 , m_isValid( other.m_isValid )
180 , m_isMonchrome( other.m_isMonchrome )
181 , m_isVisible( other.m_isVisible )
182{
183}
184
185QskGradient::~QskGradient()
186{
187}
188
189QskGradient& QskGradient::operator=( const QskGradient& other ) noexcept
190{
191 m_type = other.m_type;
192 m_spreadMode = other.m_spreadMode;
193 m_stretchMode = other.m_stretchMode;
194 m_stops = other.m_stops;
195
196 m_values[0] = other.m_values[0];
197 m_values[1] = other.m_values[1];
198 m_values[2] = other.m_values[2];
199 m_values[3] = other.m_values[3];
200 m_values[4] = other.m_values[4];
201
202 m_isDirty = other.m_isDirty;
203 m_isValid = other.m_isValid;
204 m_isMonchrome = other.m_isMonchrome;
205 m_isVisible = other.m_isVisible;
206
207 return *this;
208}
209
210bool QskGradient::operator==( const QskGradient& other ) const noexcept
211{
212 return ( m_type == other.m_type )
213 && ( m_spreadMode == other.m_spreadMode )
214 && ( m_stretchMode == other.m_stretchMode )
215 && ( m_values[0] == other.m_values[0] )
216 && ( m_values[1] == other.m_values[1] )
217 && ( m_values[2] == other.m_values[2] )
218 && ( m_values[3] == other.m_values[3] )
219 && ( m_values[4] == other.m_values[4] )
220 && ( m_stops == other.m_stops );
221}
222
223void QskGradient::updateStatusBits() const
224{
225 // doing all bits in one loop ?
226 m_isValid = qskIsGradientValid( m_stops );
227
228 if ( m_isValid )
229 {
230 m_isMonchrome = qskIsMonochrome( m_stops );
231 m_isVisible = qskIsVisible( m_stops );
232 }
233 else
234 {
235 m_isMonchrome = true;
236 m_isVisible = false;
237 }
238
239 if ( m_isVisible )
240 {
241 switch( m_type )
242 {
243 case Linear:
244 {
245 m_isVisible = !( qskFuzzyCompare( m_values[0], m_values[2] )
246 && qskFuzzyCompare( m_values[1], m_values[3] ) );
247 break;
248 }
249
250 case Radial:
251 {
252 m_isVisible = m_values[2] > 0.0 || m_values[3] > 0.0; // radius
253 break;
254 }
255
256 case Conic:
257 {
258 m_isVisible = !qFuzzyIsNull( m_values[3] ); // spanAngle
259 break;
260 }
261
262 default:
263 break;
264 }
265 }
266
267 m_isDirty = false;
268}
269
270bool QskGradient::isValid() const noexcept
271{
272 if ( m_isDirty )
273 updateStatusBits();
274
275 return m_isValid;
276}
277
278bool QskGradient::isMonochrome() const noexcept
279{
280 if ( m_isDirty )
281 updateStatusBits();
282
283 return m_isMonchrome;
284}
285
286bool QskGradient::isVisible() const noexcept
287{
288 if ( m_isDirty )
289 updateStatusBits();
290
291 return m_isVisible;
292}
293
294void QskGradient::setStops( const QColor& color )
295{
296 m_stops = { { 0.0, color }, { 1.0, color } };
297 m_isDirty = true;
298}
299
300void QskGradient::setStops( const QColor& color1, const QColor& color2 )
301{
302 m_stops = { { 0.0, color1 }, { 1.0, color2 } };
303 m_isDirty = true;
304}
305
306void QskGradient::setStops( QGradient::Preset preset )
307{
308 const auto stops = qskBuildGradientStops( QGradient( preset ).stops() );
309 setStops( stops );
310}
311
312void QskGradient::setStops( const QskGradientStops& stops )
313{
314 if ( !stops.isEmpty() && !qskIsGradientValid( stops ) )
315 {
316 qWarning( "Invalid gradient stops" );
317 m_stops.clear();
318 }
319 else
320 {
321 m_stops = stops;
322 }
323
324 m_isDirty = true;
325}
326
327int QskGradient::stepCount() const noexcept
328{
329 if ( !isValid() )
330 return 0;
331
332 auto steps = static_cast< int >( m_stops.count() ) - 1;
333
334 if ( m_stops.first().position() > 0.0 )
335 steps++;
336
337 if ( m_stops.last().position() < 1.0 )
338 steps++;
339
340 return steps;
341}
342
343qreal QskGradient::stopAt( int index ) const noexcept
344{
345 if ( index < 0 || index >= m_stops.size() )
346 return -1.0;
347
348 return m_stops[ index ].position();
349}
350
351bool QskGradient::hasStopAt( qreal value ) const noexcept
352{
353 // better use binary search TODO ...
354 for ( auto& stop : m_stops )
355 {
356 if ( stop.position() == value )
357 return true;
358
359 if ( stop.position() > value )
360 break;
361 }
362
363 return false;
364}
365
366QColor QskGradient::colorAt( int index ) const noexcept
367{
368 if ( index >= m_stops.size() )
369 return QColor();
370
371 return m_stops[ index ].color();
372}
373
374void QskGradient::setAlpha( int alpha )
375{
376 for ( auto& stop : m_stops )
377 {
378 auto c = stop.color();
379 if ( c.isValid() && c.alpha() )
380 {
381 c.setAlpha( alpha );
382 stop.setColor( c );
383 }
384 }
385
386 m_isDirty = true;
387}
388
389void QskGradient::setSpreadMode( SpreadMode spreadMode )
390{
391 m_spreadMode = spreadMode;
392}
393
394void QskGradient::setStretchMode( StretchMode stretchMode )
395{
396 m_stretchMode = stretchMode;
397}
398
399void QskGradient::stretchTo( const QRectF& rect )
400{
401 if ( m_stretchMode == NoStretch || m_type == Stops || rect.isEmpty() )
402 return; // nothing to do
403
404 const auto transform = qskTransformForRect( m_stretchMode, rect );
405
406 switch( static_cast< int >( m_type ) )
407 {
408 case Linear:
409 {
410 transform.map( m_values[0], m_values[1], &m_values[0], &m_values[1] );
411 transform.map( m_values[2], m_values[3], &m_values[2], &m_values[3] );
412
413 break;
414 }
415 case Radial:
416 {
417 transform.map( m_values[0], m_values[1], &m_values[0], &m_values[1] );
418
419 qreal rx = qMax( m_values[2], 0.0 );
420 qreal ry = qMax( m_values[3], 0.0 );
421
422 if ( rx == 0.0 || ry == 0.0 )
423 {
424 /*
425 It would be more logical if the scaling happens according
426 the width, when rx is set ad v.v. But fitting the circle is
427 probably, what most use cases need - and how to specify
428 this. Maybe by introducing another stretchMode ... TODO
429 */
430 const qreal r = qMin( rect.width(), rect.height() ) * qMax( rx, ry );
431 m_values[2] = m_values[3] = r;
432 }
433 else
434 {
435 m_values[2] = rx * rect.width();
436 m_values[3] = ry * rect.height();
437 }
438
439 break;
440 }
441 case Conic:
442 {
443 transform.map( m_values[0], m_values[1], &m_values[0], &m_values[1] );
444
445 if ( m_values[4] == 0.0 && !rect.isEmpty() )
446 m_values[4] = rect.width() / rect.height();
447
448 break;
449 }
450 }
451
452 m_stretchMode = NoStretch;
453}
454
455QskGradient QskGradient::stretchedTo( const QSizeF& size ) const
456{
457 return stretchedTo( QRectF( 0.0, 0.0, size.width(), size.height() ) );
458}
459
460QskGradient QskGradient::stretchedTo( const QRectF& rect ) const
461{
462 if ( m_stretchMode == NoStretch )
463 return *this;
464
465 QskGradient g = *this;
466 g.stretchTo( rect );
467
468 return g;
469}
470
471void QskGradient::reverse()
472{
473 if ( isMonochrome() )
474 return;
475
476 std::reverse( m_stops.begin(), m_stops.end() );
477 for( auto& stop : m_stops )
478 stop.setPosition( 1.0 - stop.position() );
479}
480
481QskGradient QskGradient::reversed() const
482{
483 auto gradient = *this;
484 gradient.reverse();
485
486 return gradient;
487}
488
489QskGradient QskGradient::extracted( qreal from, qreal to ) const
490{
491 auto gradient = *this;
492
493 if ( !isValid() || ( from > to ) || ( from > 1.0 ) )
494 {
495 gradient.clearStops();
496 }
497 else if ( isMonochrome() )
498 {
499 from = qMax( from, 0.0 );
500 to = qMin( to, 1.0 );
501
502 const auto color = m_stops.first().color();
503
504 gradient.setStops( { { from, color }, { to, color } } );
505 }
506 else
507 {
508 gradient.setStops( qskExtractedGradientStops( m_stops, from, to ) );
509 }
510
511 return gradient;
512}
513
514QskGradient QskGradient::interpolated( const QskGradient& to, qreal ratio ) const
515{
516 if ( !isValid() && !to.isValid() )
517 return to;
518
519 QskGradient gradient;
520
521 if ( qskCanBeInterpolated( *this, to ) )
522 {
523 // We simply interpolate stops and values
524
525 gradient = to;
526
527 const auto stops = qskInterpolatedGradientStops(
528 m_stops, isMonochrome(), to.m_stops, to.isMonochrome(), ratio );
529
530 gradient.setStops( stops );
531
532 for ( uint i = 0; i < sizeof( m_values ) / sizeof( m_values[0] ); i++ )
533 gradient.m_values[i] = m_values[i] + ratio * ( to.m_values[i] - m_values[i] );
534 }
535 else
536 {
537 /*
538 The interpolation is devided into 2 steps. First we
539 interpolate into a monochrome gradient and then
540 recolor the gradient towards the target gradient
541 This will always result in a smooth transition - even, when
542 interpolating between different gradient types
543 */
544
545 const auto c = QskRgb::interpolated( startColor(), to.startColor(), 0.5 );
546
547 if ( ratio < 0.5 )
548 {
549 const auto r = 2.0 * ratio;
550
551 gradient = *this;
552 gradient.setStops( qskInterpolatedGradientStops( m_stops, c, r ) );
553 }
554 else
555 {
556 const auto r = 2.0 * ( ratio - 0.5 );
557
558 gradient = to;
559 gradient.setStops( qskInterpolatedGradientStops( c, to.m_stops, r ) );
560 }
561 }
562
563 return gradient;
564}
565
566QVariant QskGradient::interpolate(
567 const QskGradient& from, const QskGradient& to, qreal progress )
568{
569 return QVariant::fromValue( from.interpolated( to, progress ) );
570}
571
572void QskGradient::clearStops()
573{
574 if ( !m_stops.isEmpty() )
575 {
576 m_stops.clear();
577 m_isDirty = true;
578 }
579}
580
581QskHashValue QskGradient::hash( QskHashValue seed ) const
582{
583 auto hash = qHash( m_type, seed );
584 hash = qHash( m_spreadMode, seed );
585 hash = qHash( m_stretchMode, seed );
586
587 if ( m_type != Stops )
588 hash = qHashBits( m_values, sizeof( m_values ), hash );
589
590 for ( const auto& stop : m_stops )
591 hash = stop.hash( hash );
592
593 return hash;
594}
595
596void QskGradient::setLinearDirection( Qt::Orientation orientation )
597{
598 setLinearDirection( QskLinearDirection( orientation ) );
599}
600
601void QskGradient::setLinearDirection( qreal x1, qreal y1, qreal x2, qreal y2 )
602{
603 setLinearDirection( QskLinearDirection( x1, y1, x2, y2 ) );
604}
605
606void QskGradient::setLinearDirection( const QskLinearDirection& direction )
607{
608 m_type = Linear;
609
610 m_values[0] = direction.x1();
611 m_values[1] = direction.y1();
612 m_values[2] = direction.x2();
613 m_values[3] = direction.y2();
614}
615
616QskLinearDirection QskGradient::linearDirection() const
617{
618 if ( m_type != Linear )
619 {
620 qWarning() << "Requesting linear attributes from a non linear gradient.";
621 return QskLinearDirection( 0.0, 0.0, 0.0, 0.0 );
622 }
623
624 return QskLinearDirection( m_values[0], m_values[1], m_values[2], m_values[3] );
625}
626
627void QskGradient::setRadialDirection( const qreal x, qreal y, qreal radius )
628{
629 setRadialDirection( QskRadialDirection( x, y, radius ) );
630}
631
632void QskGradient::setRadialDirection( const qreal x, qreal y,qreal radiusX, qreal radiusY )
633{
634 setRadialDirection( QskRadialDirection( x, y, radiusX, radiusY ) );
635}
636void QskGradient::setRadialDirection( const QskRadialDirection& direction )
637{
638 m_type = Radial;
639
640 m_values[0] = direction.center().x();
641 m_values[1] = direction.center().y();
642 m_values[2] = direction.radiusX();
643 m_values[3] = direction.radiusY();
644}
645
646QskRadialDirection QskGradient::radialDirection() const
647{
648 if ( m_type != Radial )
649 {
650 qWarning() << "Requesting radial attributes from a non radial gradient.";
651 return QskRadialDirection( 0.5, 0.5, 0.0 );
652 }
653
654 return QskRadialDirection( m_values[0], m_values[1], m_values[2], m_values[3] );
655}
656
657void QskGradient::setConicDirection( qreal x, qreal y )
658{
659 setConicDirection( QskConicDirection( x, y ) );
660}
661
662void QskGradient::setConicDirection( qreal x, qreal y,
663 qreal startAngle, qreal spanAngle )
664{
665 setConicDirection( QskConicDirection( x, y, startAngle, spanAngle ) );
666}
667
668void QskGradient::setConicDirection( qreal x, qreal y,
669 qreal startAngle, qreal spanAngle, qreal aspectRatio )
670{
671 const QskConicDirection dir( x, y, startAngle, spanAngle, aspectRatio );
672 setConicDirection( dir );
673}
674
675void QskGradient::setConicDirection( const QskConicDirection& direction )
676{
677 m_type = Conic;
678
679 m_values[0] = direction.center().x();
680 m_values[1] = direction.center().y();
681 m_values[2] = direction.startAngle();
682 m_values[3] = direction.spanAngle();
683 m_values[4] = direction.aspectRatio();
684}
685
686QskConicDirection QskGradient::conicDirection() const
687{
688 if ( m_type != Conic )
689 {
690 qWarning() << "Requesting conic attributes from a non conic gradient.";
691 return QskConicDirection( 0.5, 0.5, 0.0, 0.0 );
692 }
693
694 QskConicDirection dir( m_values[0], m_values[1], m_values[2], m_values[3] );
695 dir.setAspectRatio( m_values[4] );
696
697 return dir;
698}
699
700void QskGradient::setDirection( Type type )
701{
702 switch( type )
703 {
704 case Linear:
705 setLinearDirection( QskLinearDirection() );
706 break;
707
708 case Radial:
709 setRadialDirection( QskRadialDirection() );
710 break;
711
712 case Conic:
713 setConicDirection( QskConicDirection() );
714 break;
715
716 case Stops:
717 resetDirection();
718 break;
719 }
720}
721
722void QskGradient::resetDirection()
723{
724 m_type = Stops;
725 m_values[0] = m_values[1] = m_values[2] = m_values[3] = m_values[4] = 0.0;
726}
727
728QGradient QskGradient::toQGradient() const
729{
730 QGradient g;
731
732 switch( static_cast< int >( m_type ) )
733 {
734 case Linear:
735 {
736 g = QLinearGradient( m_values[0], m_values[1], m_values[2], m_values[3] );
737 break;
738 }
739
740 case Radial:
741 {
742 g = QRadialGradient( m_values[0], m_values[1], m_values[2] );
743 break;
744 }
745
746 case Conic:
747 {
748 if ( m_values[3] != 360.0 )
749 {
750 qWarning() <<
751 "QskGradient: spanAngle got lost, when converting to QConicalGradient";
752 }
753
754 g = QConicalGradient( m_values[0], m_values[1], m_values[2] );
755 break;
756 }
757 }
758
759 g.setCoordinateMode( m_stretchMode == NoStretch
760 ? QGradient::LogicalMode : QGradient::ObjectMode );
761
762 g.setSpread( static_cast< QGradient::Spread >( m_spreadMode ) );
763 g.setStops( qskToQGradientStops( m_stops ) );
764
765 return g;
766}
767
768#ifndef QT_NO_DEBUG_STREAM
769
770#include <qdebug.h>
771
772QDebug operator<<( QDebug debug, const QskGradient& gradient )
773{
774 QDebugStateSaver saver( debug );
775 debug.nospace();
776
777 debug << "QskGradient(";
778
779 switch ( gradient.type() )
780 {
781 case QskGradient::Linear:
782 {
783 debug << " L(";
784
785 const auto dir = gradient.linearDirection();
786 debug << dir.start().x() << "," << dir.start().y()
787 << "," << dir.stop().x() << "," << dir.stop().y() << ")";
788
789 break;
790 }
791
792 case QskGradient::Radial:
793 {
794 debug << " R(";
795
796 const auto dir = gradient.radialDirection();
797
798 debug << dir.center().x() << "," << dir.center().y()
799 << "," << dir.radiusX() << dir.radiusY() << ")";
800
801 break;
802 }
803
804 case QskGradient::Conic:
805 {
806 debug << " C(";
807
808 const auto dir = gradient.conicDirection();
809
810 debug << dir.center().x() << "," << dir.center().y()
811 << ",AR:" << dir.aspectRatio()
812 << ",[" << dir.startAngle() << "," << dir.spanAngle() << "])";
813 break;
814 }
815
816 case QskGradient::Stops:
817 {
818 break;
819 }
820 }
821
822 if ( !gradient.stops().isEmpty() )
823 {
824 if ( gradient.isMonochrome() )
825 {
826 debug << ' ';
827 QskRgb::debugColor( debug, gradient.rgbStart() );
828 }
829 else
830 {
831 debug << " ( ";
832
833 const auto& stops = gradient.stops();
834 for ( int i = 0; i < stops.count(); i++ )
835 {
836 if ( i != 0 )
837 debug << ", ";
838
839 debug << stops[i];
840 }
841
842 debug << " )";
843 }
844 }
845
846 if ( gradient.stretchMode() == QskGradient::StretchToSize )
847 {
848 debug << " SS";
849 }
850
851 switch( gradient.spreadMode() )
852 {
853 case QskGradient::RepeatSpread:
854 debug << " RP";
855 break;
856
857 case QskGradient::ReflectSpread:
858 debug << " RF";
859 break;
860
861 case QskGradient::PadSpread:
862 break;
863 }
864
865 debug << " )";
866
867 return debug;
868}
869
870#endif
871
872#include "moc_QskGradient.cpp"