/****************************************************************************** * QSkinny - Copyright (C) 2016 Uwe Rathmann * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ #include "QskBoxGradientStroker.h" #include "QskBoxBasicStroker.h" #include "QskVertex.h" #include "QskBoxColorMap.h" #include "QskBoxMetrics.h" static inline bool qskCanUseHVFiller( const Qt::Orientations orientations, const QskLinearDirection& dir ) { if ( !dir.isTilted() ) return orientations & ( dir.isVertical() ? Qt::Vertical : Qt::Horizontal ); return false; } namespace { using namespace QskVertex; class Value { public: qreal from, to; // opposite to the direction of the gradient qreal pos; // in direction of the gradient }; class FillerHV { public: FillerHV( const QskBoxMetrics& metrics ) : m_metrics( metrics ) { } int setLines( const QskGradient& gradient, ColoredLine* lines ) { using namespace Qt; const auto corners = m_metrics.corners; const auto dir = gradient.linearDirection(); m_isVertical = dir.isVertical(); m_t0 = m_isVertical ? dir.y1() : dir.x1(); m_dt = m_isVertical ? dir.dy() : dir.dx(); ColoredLine* l = lines; Value v1, v2; ArcIterator arcIt; m_gradientIterator.reset( gradient.stops() ); m_c1 = &corners[ Qt::TopLeftCorner ]; m_c2 = &corners[ m_isVertical ? Qt::TopRightCorner : Qt::BottomLeftCorner ]; m_c3 = ( m_c1->stepCount > m_c2->stepCount ) ? m_c1 : m_c2; arcIt.reset( m_c3->stepCount, !m_isVertical ); v2 = contourValue( arcIt ); skipGradientLines( v2.pos ); setContourLine( v2, l++ ); arcIt.increment(); do { v1 = v2; v2 = contourValue( arcIt ); l = setGradientLines( v1, v2, l ); setContourLine( v2, l++ ); arcIt.increment(); } while( !arcIt.isDone() ); m_c1 = &corners[ m_isVertical ? Qt::BottomLeftCorner : Qt::TopRightCorner ]; m_c2 = &corners[ Qt::BottomRightCorner ]; m_c3 = ( m_c1->stepCount > m_c2->stepCount ) ? m_c1 : m_c2; arcIt.reset( m_c3->stepCount, m_isVertical ); if ( contourValue( arcIt ).pos <= v2.pos ) arcIt.increment(); // ellipse: opening/closing parts are connected do { v1 = v2; v2 = contourValue( arcIt ); l = setGradientLines( v1, v2, l ); setContourLine( v2, l++ ); arcIt.increment(); } while( !arcIt.isDone() ); return l - lines; } inline void skipGradientLines( qreal pos ) { while ( !m_gradientIterator.isDone() ) { if ( m_t0 + m_gradientIterator.position() * m_dt > pos ) return; m_gradientIterator.advance(); } } inline ColoredLine* setGradientLines( const Value& v1, const Value& v2, ColoredLine* lines ) { while ( !m_gradientIterator.isDone() ) { const auto pos = m_t0 + m_gradientIterator.position() * m_dt; if ( pos > v2.pos || qFuzzyIsNull( v2.pos - pos ) ) return lines; const auto color = m_gradientIterator.color(); const qreal f = ( pos - v1.pos ) / ( v2.pos - v1.pos ); const qreal t1 = v1.from + f * ( v2.from - v1.from ); const qreal t2 = v1.to + f * ( v2.to - v1.to ); setLine( t1, t2, pos, color, lines++ ); m_gradientIterator.advance(); } return lines; } inline void setContourLine( const Value& v, ColoredLine* line ) { const auto color = m_gradientIterator.colorAt( ( v.pos - m_t0 ) / m_dt ); setLine( v.from, v.to, v.pos, color, line ); } private: inline Value contourValue( const ArcIterator& arcIt ) const { const auto cos = arcIt.cos(); const auto sin = arcIt.sin(); if ( m_isVertical ) return { m_c1->xInner( cos ), m_c2->xInner( cos ), m_c3->yInner( sin ) }; else return { m_c1->yInner( sin ), m_c2->yInner( sin ), m_c3->xInner( cos ) }; } inline void setLine( qreal from, qreal to, qreal pos, Color color, ColoredLine* line ) { if ( m_isVertical ) line->setLine( from, pos, to, pos, color ); else line->setLine( pos, from, pos, to, color ); } const QskBoxMetrics& m_metrics; bool m_isVertical; qreal m_t0, m_dt; const QskBoxMetrics::Corner* m_c1, * m_c2, * m_c3; QskBox::GradientIterator m_gradientIterator; }; } namespace { using namespace QskVertex; class Point { public: Point() = default; inline Point( qreal x, qreal y, qreal v ): x( x ), y( y ), v( v ) {} qreal x = 0; qreal y = 0; qreal v = 0; // value at (x,y) according the gradient vector in the range [0,1] }; /* Iterating over the interpolating ( ~tangent ) lines of the outline in order of increasing gradient values. We have the options to run clockwise or counter-clockwise */ class TangentLineIterator { public: TangentLineIterator( const QskBoxMetrics& metrics, const QskLinearDirection& dir, bool isClockwise ) : m_metrics( metrics ) , m_dir( dir ) , m_isClockwise( isClockwise ) { using namespace Qt; if ( dir.dx() >= 0.0 ) { if ( dir.dy() >= 0.0 ) { m_corners[0] = TopLeftCorner; m_corners[1] = m_isClockwise ? TopRightCorner : BottomLeftCorner; m_corners[2] = BottomRightCorner; } else { m_corners[0] = BottomLeftCorner; m_corners[1] = m_isClockwise ? BottomRightCorner : TopLeftCorner; m_corners[2] = TopRightCorner; } } else { if ( dir.dy() >= 0.0 ) { m_corners[0] = TopRightCorner; m_corners[1] = m_isClockwise ? TopLeftCorner : BottomRightCorner; m_corners[2] = BottomLeftCorner; } else { m_corners[0] = BottomRightCorner; m_corners[1] = m_isClockwise ? BottomLeftCorner : TopRightCorner; m_corners[2] = TopLeftCorner; } } } inline QPointF pointAt( qreal value ) const { const auto dv = m_p2.v - m_p1.v; if ( dv == 0.0 ) return QPointF( m_p1.x, m_p1.y ); const qreal r = ( value - m_p1.v ) / dv; return QPointF( m_p1.x + r * ( m_p2.x - m_p1.x ), m_p1.y + r * ( m_p2.y - m_p1.y ) ); } void setup( const QskVertex::ArcIterator& arcIterator ) { m_arcIterator = arcIterator; const auto& c = m_metrics.corners[ m_corners[0] ]; const qreal x = c.xInner( m_arcIterator.cos() ); const qreal y = c.yInner( m_arcIterator.sin() ); m_p2 = m_p1 = { x, y, m_dir.valueAt( x, y ) }; } inline bool isDone() const { return m_isDone; } inline qreal value() const { return m_p2.v; } inline Qt::Corner corner() const { return m_corners[ m_cornerIndex ]; } inline Point p1() const { return m_p1; } inline Point p2() const { return m_p2; } void incrementTo( qreal value ) { while ( !isDone() && ( value > m_p2.v ) ) advance(); } inline void advance() { if ( m_isDone ) return; m_arcIterator.increment(); auto c = &m_metrics.corners[ m_corners[ m_cornerIndex ] ]; if( m_arcIterator.isDone() ) { if ( m_cornerIndex < 2 ) { c = &m_metrics.corners[ m_corners[ ++m_cornerIndex ] ]; m_arcIterator.reset( c->innerStepCount(), !m_arcIterator.isInverted() ); } } m_p1 = m_p2; m_p2.x = c->xInner( m_arcIterator.cos() ); m_p2.y = c->yInner( m_arcIterator.sin() ); m_p2.v = m_dir.valueAt( m_p2.x, m_p2.y ); if ( m_cornerIndex == 2 ) { if ( ( m_p2.v < m_p1.v ) || m_arcIterator.isDone() ) { // passing the opposite position from our starting point m_p2 = m_p1; m_isDone = true; } } } private: const QskBoxMetrics& m_metrics; const QskLinearDirection m_dir; const bool m_isClockwise; bool m_isDone = false; QskVertex::ArcIterator m_arcIterator; Point m_p1, m_p2; int m_cornerIndex = 0; Qt::Corner m_corners[3]; }; /* OutlineIterator iterates a pair of tangent lines in direction of the gradient vector. The second tangent line increases in oppsite direction so that the value of the leading point of the first tangent line is in between the values of the opposite tangent line. Contour and gradient lines can then be found from cutting the tangent lines with the perpendicular of the gradient vector. */ class OutlineIterator { public: OutlineIterator( const QskBoxMetrics& metrics, const QskLinearDirection& dir, bool clockwise ) : m_isVertical( dir.dx() == 0.0 ) , m_it1( metrics, dir, clockwise ) , m_it2( metrics, dir, !clockwise ) { const auto corner = m_it1.corner(); const auto& c = metrics.corners[ m_it1.corner() ]; bool inverted = clockwise; if ( corner == Qt::TopLeftCorner || corner == Qt::BottomRightCorner ) inverted = clockwise; else inverted = !clockwise; QskVertex::ArcIterator arcIt; arcIt.reset( c.innerStepCount(), inverted ); if ( c.innerStepCount() == 1 ) { m_it1.setup( arcIt ); m_it2.setup( arcIt.reverted() ); // not rounded m_it1.advance(); m_it2.advance(); } else { arcIt.reset( c.innerStepCount(), clockwise ); qreal v1 = dir.valueAt( c.xInner( arcIt.cos() ), c.yInner( arcIt.sin() ) ); do { arcIt.increment(); const qreal v2 = dir.valueAt( c.xInner( arcIt.cos() ), c.yInner( arcIt.sin() ) ); if ( v2 > v1 ) break; v1 = v2; } while( !arcIt.isDone() ); arcIt.decrement(); m_it1.setup( arcIt ); m_it2.setup( arcIt.reverted() ); } m_it2.incrementTo( m_it1.value() ); } inline void advance() { m_it1.advance(); /* Increasing the opposite iterator until its value becomes larger than the value of the leading iterator. Then the opposite point for the next line can be found by interpolating between p1/p2 of the opposite iterator. */ if ( !m_it1.isDone() ) m_it2.incrementTo( m_it1.value() ); } inline bool isDone() const { return m_it1.isDone(); } inline qreal value() const { return m_it1.value(); } inline void setLineAt( qreal value, Color color, ColoredLine* line ) { const auto p1 = m_it1.pointAt( value ); const auto p2 = m_it2.pointAt( value ); setLine( p1.x(), p1.y(), p2.x(), p2.y(), color, line ); } inline void setLine( Color color, ColoredLine* line ) { const auto p1 = m_it1.p2(); const auto p2 = m_it2.pointAt( p1.v ); setLine( p1.x, p1.y, p2.x(), p2.y(), color, line ); } private: inline void setLine( qreal x1, qreal y1, qreal x2, qreal y2, Color color, ColoredLine* line ) { if ( m_isVertical ) { if ( x1 < x2 ) line->setLine( x1, y1, x2, y2, color ); else line->setLine( x2, y2, x1, y1, color ); } else { if ( y1 < y2 ) line->setLine( x1, y1, x2, y2, color ); else line->setLine( x2, y2, x1, y1, color ); } } /* The first iterator for running along the left or right half of the ellipse. The other one is for finding the corresponing point at the other side. */ const bool m_isVertical; TangentLineIterator m_it1, m_it2; }; /* ContourIterator runs a pair of OutlineIterators, one clockwise the other counter clockwise in direction of the gradient vector, so that we always pass all corners. The next contour line can always be found from the outline iterator with the larger gradient value. Gradient lines can then be found from interpolating between the tangent lines of the outline iterators. */ class ContourIterator { public: ContourIterator( const QskBoxMetrics& metrics, const QskLinearDirection& dir ) : m_left( metrics, dir, false ) , m_right( metrics, dir, true ) { m_right.advance(); m_next = &m_left; } inline void setGradientLine( qreal value, Color color, ColoredLine* line ) { m_next->setLineAt( value, color, line ); } inline void setContourLine( Color color, ColoredLine* line ) { m_next->setLine( color, line ); } inline qreal value() const { return m_next->value(); } inline bool advance() { if ( qFuzzyIsNull( m_left.value() - m_right.value() ) ) { m_left.advance(); m_right.advance(); } else { m_next->advance(); } if ( m_next->isDone() ) return false; m_next = ( m_left.value() < m_right.value() ) ? &m_left : &m_right; return true; } private: OutlineIterator m_left, m_right; OutlineIterator* m_next; }; class FillerD { public: FillerD( const QskBoxMetrics& metrics ) : m_metrics( metrics ) { } int setLines( const QskGradient& gradient, ColoredLine* lines ) { ContourIterator it( m_metrics, gradient.linearDirection() ); QskBox::GradientIterator gradientIt( gradient.stops() ); ColoredLine* l = lines; // skip leading gradient lines while ( !gradientIt.isDone() ) { const auto pos = gradientIt.position(); if ( pos > it.value() || qFuzzyIsNull( pos - it.value() ) ) break; gradientIt.advance(); } do { // gradient lines while ( !gradientIt.isDone() ) { const auto pos = gradientIt.position(); if ( pos > it.value() || qFuzzyIsNull( pos - it.value() ) ) break; it.setGradientLine( pos, gradientIt.color(), l++ ); gradientIt.advance(); } // contour line const auto color = gradientIt.colorAt( it.value() ); it.setContourLine( color, l++ ); } while ( it.advance() ); return l - lines; } private: const QskBoxMetrics& m_metrics; }; class FillerRect { public: FillerRect( const QskBoxMetrics& metrics ) : m_metrics( metrics ) { } int setLines( const QskGradient& gradient, ColoredLine* lines ) const { const qreal x1 = m_metrics.innerRect.left(); const qreal x2 = m_metrics.innerRect.right(); const qreal y1 = m_metrics.innerRect.top(); const qreal y2 = m_metrics.innerRect.bottom(); QskBox::GradientIterator it( gradient.stops() ); ColoredLine* l = lines; const auto dir = gradient.linearDirection(); if ( dir.isTilted() ) { const qreal m = dir.dy() / dir.dx(); const auto vec = dir.vector(); struct { qreal x, y, value; } c1, c2, c3, c4; { // corners sorted in order their values c1 = { x1, y1, dir.valueAt( x1, y1 ) }; c2 = { x2, y1, dir.valueAt( x2, y1 ) }; c3 = { x1, y2, dir.valueAt( x1, y2 ) }; c4 = { x2, y2, dir.valueAt( x2, y2 ) }; if ( m < 0.0 ) { qSwap( c1, c3 ); qSwap( c2, c4 ); } if ( c1.value > c4.value ) qSwap( c1, c4 ); if ( c2.value > c3.value ) qSwap( c2, c3 ); } // skipping all gradient lines before the first corner while ( !it.isDone() && ( it.position() <= c1.value ) ) it.advance(); setLine( c1.x, c1.y, c1.x, c1.y, it.colorAt( c1.value ), l++ ); while ( !it.isDone() && ( it.position() < c2.value ) ) { const auto p = vec.pointAt( it.position() ); const qreal ly1 = p.y() + ( p.x() - c1.x ) / m; const qreal lx2 = p.x() + ( p.y() - c1.y ) * m; setLine( c1.x, ly1, lx2, c1.y, it.color(), l++ ); it.advance(); } if ( c1.x == c3.x ) // cutting left/right edges { const auto dy = ( c2.x - c3.x ) / m; setLine( c2.x, c2.y, c3.x, c2.y + dy, it.colorAt( c2.value ), l++ ); while ( !it.isDone() && ( it.position() < c3.value ) ) { const auto p = vec.pointAt( it.position() ); const qreal ly1 = p.y() + ( p.x() - c2.x ) / m; const qreal ly2 = p.y() + ( p.x() - c3.x ) / m; setLine( c2.x, ly1, c3.x, ly2, it.color(), l++ ); it.advance(); } setLine( c2.x, c3.y - dy, c3.x, c3.y, it.colorAt( c3.value ), l++ ); } else // cutting top/bottom edges { const qreal dx = ( c2.y - c3.y ) * m; setLine( c2.x, c2.y, c2.x + dx, c3.y, it.colorAt( c2.value ), l++ ); while ( !it.isDone() && ( it.position() < c3.value ) ) { const auto p = vec.pointAt( it.position() ); const qreal lx1 = p.x() + ( p.y() - c2.y ) * m; const qreal lx2 = p.x() + ( p.y() - c3.y ) * m; setLine( lx1, c2.y, lx2, c3.y, it.color(), l++ ); it.advance(); } setLine( c3.x - dx, c2.y, c3.x, c3.y, it.colorAt( c3.value ), l++ ); } while ( !it.isDone() && ( it.position() < c4.value ) ) { const auto p = vec.pointAt( it.position() ); const qreal ly1 = p.y() + ( p.x() - c4.x ) / m; const qreal lx2 = p.x() + ( p.y() - c4.y ) * m; setLine( c4.x, ly1, lx2, c4.y, it.color(), l++ ); it.advance(); } setLine( c4.x, c4.y, c4.x, c4.y, it.colorAt( c4.value ), l++ ); } else if ( dir.isVertical() ) { Q_ASSERT( dir.dy() > 0.0 ); // normalized in QskBoxRenderer const qreal min = ( y1 - dir.y1() ) / dir.dy(); const qreal max = ( y2 - dir.y1() ) / dir.dy(); while ( !it.isDone() && ( it.position() <= min ) ) it.advance(); setHLine( y1, it.colorAt( min ), l++ ); while ( !it.isDone() && ( it.position() < max ) ) { const auto y = dir.y1() + it.position() * dir.dy(); setHLine( y, it.color(), l++ ); it.advance(); } setHLine( y2, it.colorAt( max ), l++ ); } else // dir.isHorizontal { Q_ASSERT( dir.dx() > 0.0 ); // normalized in QskBoxRenderer const qreal min = ( x1 - dir.x1() ) / dir.dx(); const qreal max = ( x2 - dir.x1() ) / dir.dx(); while ( !it.isDone() && ( it.position() <= min ) ) it.advance(); setVLine( x1, it.colorAt( min ), l++ ); while ( !it.isDone() && ( it.position() < max ) ) { const auto x = dir.x1() + it.position() * dir.dx(); setVLine( x, it.color(), l++ ); it.advance(); } setVLine( x2, it.colorAt( max ), l++ ); } return l - lines; } private: inline void setHLine( qreal y, QskVertex::Color color, QskVertex::ColoredLine* line ) const { const auto& r = m_metrics.innerRect; line->setLine( r.left(), y, r.right(), y, color ); } inline void setVLine( qreal x, QskVertex::Color color, QskVertex::ColoredLine* line ) const { const auto& r = m_metrics.innerRect; line->setLine( x, r.top(), x, r.bottom(), color ); } inline void setLine( qreal x1, qreal y1, qreal x2, qreal y2, Color color, ColoredLine* line ) const { if ( x1 <= x2 ) line->setLine( x1, y1, x2, y2, color ); else line->setLine( x2, y2, x1, y1, color ); } const QskBoxMetrics& m_metrics; }; } QskBoxGradientStroker::QskBoxGradientStroker( const QskBoxMetrics& metrics, const QskGradient& gradient ) : m_metrics( metrics ) , m_gradient( gradient ) , m_dir( m_gradient.linearDirection() ) { } int QskBoxGradientStroker::lineCount() const { if ( m_metrics.innerRect.isEmpty() || !m_gradient.isVisible() ) return 0; int n = m_gradient.stepCount() - 1; if ( m_metrics.isInsideRounded ) { if ( qskCanUseHVFiller( m_metrics.stepSymmetries, m_dir ) ) { const QskBoxBasicStroker stroker( m_metrics, QskBoxBorderColors(), m_gradient ); n += stroker.fillCount(); } else { n += m_metrics.innerStepCount() + 4; } } else { n += 2; if ( m_dir.isTilted() ) n += 2; // contour lines for the opposite corners } /* The gradient starts and/or ends inside of the rectangle and we have to add pad with extra gradient lines. */ if ( !m_dir.contains( m_metrics.innerRect ) ) n += 2; return n; } void QskBoxGradientStroker::setLines( int lineCount, QskVertex::ColoredLine* lines ) { int effectiveCount; if ( m_metrics.isInsideRounded ) { if ( qskCanUseHVFiller( m_metrics.stepSymmetries, m_dir ) ) { FillerHV filler( m_metrics ); effectiveCount = filler.setLines( m_gradient, lines ); } else { FillerD filler( m_metrics ); effectiveCount = filler.setLines( m_gradient, lines ); } } else // rectangle { FillerRect filler( m_metrics ); effectiveCount = filler.setLines( m_gradient, lines ); } #if 0 qDebug() << "expected:" << lineCount << ", got:" << effectiveCount; #endif if ( lineCount > 0 ) { if ( effectiveCount > lineCount ) { qFatal( "geometry: allocated memory exceeded: %d vs. %d", effectiveCount, lineCount ); } if ( effectiveCount < lineCount ) { /* Gradient or contour lines might be at the same position and we end up with less lines, than expected. As it is hard to precalculate all corner cases we allow a defensive allocaton policy and simply fill up the line buffer with duplicates of the last line. */ for ( int i = effectiveCount; i < lineCount; i++ ) lines[i] = lines[i-1]; } } }