From 9e99735d2f3611f1ee1151426a719b3c685528e7 Mon Sep 17 00:00:00 2001 From: Uwe Rathmann Date: Tue, 18 Apr 2023 16:08:37 +0200 Subject: [PATCH] aspectRatio for conic gradients added to support f.e for filling ellipsoid arcs. --- src/common/QskGradient.cpp | 24 ++++++++++++++-- src/common/QskGradient.h | 14 +++++++--- src/common/QskGradientDirection.cpp | 5 ++++ src/common/QskGradientDirection.h | 27 +++++++++++++----- src/nodes/QskGradientMaterial.cpp | 29 +++++++++++++++++--- src/nodes/shaders/gradientconic-vulkan.frag | 1 + src/nodes/shaders/gradientconic-vulkan.vert | 3 ++ src/nodes/shaders/gradientconic.frag.qsb | Bin 1803 -> 1830 bytes src/nodes/shaders/gradientconic.vert | 3 ++ src/nodes/shaders/gradientconic.vert.qsb | Bin 1503 -> 1617 bytes 10 files changed, 88 insertions(+), 18 deletions(-) diff --git a/src/common/QskGradient.cpp b/src/common/QskGradient.cpp index 54acf4d9..31e18a0a 100644 --- a/src/common/QskGradient.cpp +++ b/src/common/QskGradient.cpp @@ -171,7 +171,7 @@ QskGradient::QskGradient( const QGradient& qGradient ) QskGradient::QskGradient( const QskGradient& other ) noexcept : m_stops( other.m_stops ) , m_values{ other.m_values[0], other.m_values[1], - other.m_values[2], other.m_values[3], } + other.m_values[2], other.m_values[3], other.m_values[4] } , m_type( other.m_type ) , m_spreadMode( other.m_spreadMode ) , m_stretchMode( other.m_stretchMode ) @@ -197,6 +197,7 @@ QskGradient& QskGradient::operator=( const QskGradient& other ) noexcept m_values[1] = other.m_values[1]; m_values[2] = other.m_values[2]; m_values[3] = other.m_values[3]; + m_values[4] = other.m_values[4]; m_isDirty = other.m_isDirty; m_isValid = other.m_isValid; @@ -215,6 +216,7 @@ bool QskGradient::operator==( const QskGradient& other ) const noexcept && ( m_values[1] == other.m_values[1] ) && ( m_values[2] == other.m_values[2] ) && ( m_values[3] == other.m_values[3] ) + && ( m_values[4] == other.m_values[4] ) && ( m_stops == other.m_stops ); } @@ -439,6 +441,10 @@ void QskGradient::stretchTo( const QRectF& rect ) case Conic: { transform.map( m_values[0], m_values[1], &m_values[0], &m_values[1] ); + + if ( m_values[4] == 0.0 && !rect.isEmpty() ) + m_values[4] = rect.width() / rect.height(); + break; } } @@ -657,6 +663,13 @@ void QskGradient::setConicDirection( qreal x, qreal y, setConicDirection( QskConicDirection( x, y, startAngle, spanAngle ) ); } +void QskGradient::setConicDirection( qreal x, qreal y, + qreal startAngle, qreal spanAngle, qreal aspectRatio ) +{ + const QskConicDirection dir( x, y, startAngle, spanAngle, aspectRatio ); + setConicDirection( dir ); +} + void QskGradient::setConicDirection( const QskConicDirection& direction ) { m_type = Conic; @@ -665,6 +678,7 @@ void QskGradient::setConicDirection( const QskConicDirection& direction ) m_values[1] = direction.center().y(); m_values[2] = direction.startAngle(); m_values[3] = direction.spanAngle(); + m_values[4] = direction.aspectRatio(); } QskConicDirection QskGradient::conicDirection() const @@ -674,7 +688,10 @@ QskConicDirection QskGradient::conicDirection() const if ( m_type != Conic ) return QskConicDirection( 0.5, 0.5, 0.0, 0.0 ); - return QskConicDirection( m_values[0], m_values[1], m_values[2], m_values[3] ); + QskConicDirection dir( m_values[0], m_values[1], m_values[2], m_values[3] ); + dir.setAspectRatio( m_values[4] ); + + return dir; } void QskGradient::setDirection( Type type ) @@ -702,7 +719,7 @@ void QskGradient::setDirection( Type type ) void QskGradient::resetDirection() { m_type = Stops; - m_values[0] = m_values[1] = m_values[2] = m_values[3] = 0.0; + m_values[0] = m_values[1] = m_values[2] = m_values[3] = m_values[4] = 0.0; } QskGradient QskGradient::effectiveGradient() const @@ -803,6 +820,7 @@ QDebug operator<<( QDebug debug, const QskGradient& gradient ) const auto dir = gradient.conicDirection(); debug << dir.center().x() << "," << dir.center().y() + << ",AR:" << dir.aspectRatio() << ",[" << dir.startAngle() << "," << dir.spanAngle() << "])"; break; } diff --git a/src/common/QskGradient.h b/src/common/QskGradient.h index 8fb58ccd..5df88b8c 100644 --- a/src/common/QskGradient.h +++ b/src/common/QskGradient.h @@ -97,8 +97,14 @@ class QSK_EXPORT QskGradient void setRadialDirection( const qreal x, qreal y, qreal radiusX, qreal radiusY ); QskRadialDirection radialDirection() const; - void setConicDirection( qreal, qreal ); - void setConicDirection( qreal, qreal, qreal, qreal = 360.0 ); + void setConicDirection( qreal x, qreal y ); + + void setConicDirection( qreal x, qreal y, + qreal startAngle, qreal spanAngle = 360.0 ); + + void setConicDirection( qreal x, qreal y, + qreal startAngle, qreal spanAngle, qreal aspectRatio ); + void setConicDirection( const QskConicDirection& ); QskConicDirection conicDirection() const; @@ -172,9 +178,9 @@ class QSK_EXPORT QskGradient /* Linear: x1, y1, x2, y2 Radial: centerX, centerY, radiusX, radiusY - Conic: centerX, centerY, startAngle, spanAngle + Conic: centerX, centerY, startAngle, spanAngle, aspectRatio */ - qreal m_values[4] = {}; + qreal m_values[5] = {}; unsigned int m_type : 3; unsigned int m_spreadMode : 3; diff --git a/src/common/QskGradientDirection.cpp b/src/common/QskGradientDirection.cpp index 115edd2b..900377b5 100644 --- a/src/common/QskGradientDirection.cpp +++ b/src/common/QskGradientDirection.cpp @@ -228,6 +228,11 @@ void QskConicDirection::setSpanAngle( qreal degrees ) noexcept m_spanAngle = qBound( -360.0, degrees, 360.0 ); } +void QskConicDirection::setAspectRatio( qreal ratio ) noexcept +{ + m_aspectRatio = qMax( ratio, 0.0 ); +} + // -- QskRadialDirection void QskRadialDirection::setCenter( const QPointF& center ) noexcept diff --git a/src/common/QskGradientDirection.h b/src/common/QskGradientDirection.h index d8a74f52..4ba3e1ad 100644 --- a/src/common/QskGradientDirection.h +++ b/src/common/QskGradientDirection.h @@ -105,16 +105,19 @@ class QSK_EXPORT QskConicDirection Q_PROPERTY( qreal y READ y WRITE setY ) Q_PROPERTY( qreal startAngle READ startAngle WRITE setStartAngle ) Q_PROPERTY( qreal spanAngle READ spanAngle WRITE setSpanAngle ) + Q_PROPERTY( qreal aspectRatio READ aspectRatio WRITE setAspectRatio ) public: // counter-clockwise constexpr QskConicDirection() noexcept = default; constexpr QskConicDirection( qreal x, qreal y, qreal startAngle = 0.0 ) noexcept; - constexpr QskConicDirection( qreal x, qreal y, qreal startAngle, qreal spanAngle ) noexcept; + constexpr QskConicDirection( qreal x, qreal y, + qreal startAngle, qreal spanAngle, qreal aspectRatio = 0.0 ) noexcept; constexpr QskConicDirection( const QPointF&, qreal startAngle = 0.0 ) noexcept; - constexpr QskConicDirection( const QPointF&, qreal startAngle, qreal spanAngle ) noexcept; + constexpr QskConicDirection( const QPointF&, + qreal startAngle, qreal spanAngle, qreal aspectRatio = 0.0 ) noexcept; constexpr QPointF center() const noexcept; void setCenter(const QPointF& center) noexcept; @@ -132,11 +135,15 @@ class QSK_EXPORT QskConicDirection constexpr qreal spanAngle() const noexcept; void setSpanAngle( qreal ) noexcept; + constexpr qreal aspectRatio() const noexcept; + void setAspectRatio( qreal ) noexcept; + private: qreal m_x = 0.5; qreal m_y = 0.5; qreal m_startAngle = 0.0; qreal m_spanAngle = 360.0; + qreal m_aspectRatio = 0.0; }; class QSK_EXPORT QskRadialDirection @@ -299,12 +306,13 @@ inline constexpr QskConicDirection::QskConicDirection( { } -inline constexpr QskConicDirection::QskConicDirection( - qreal x, qreal y, qreal startAngle, qreal spanAngle ) noexcept +inline constexpr QskConicDirection::QskConicDirection( qreal x, qreal y, + qreal startAngle, qreal spanAngle, qreal aspectRatio ) noexcept : m_x( x ) , m_y( y ) , m_startAngle( startAngle ) , m_spanAngle( spanAngle ) + , m_aspectRatio( aspectRatio ) { } @@ -314,9 +322,9 @@ inline constexpr QskConicDirection::QskConicDirection( { } -inline constexpr QskConicDirection::QskConicDirection( - const QPointF& pos, qreal startAngle, qreal spanAngle ) noexcept - : QskConicDirection( pos.x(), pos.y(), startAngle, spanAngle ) +inline constexpr QskConicDirection::QskConicDirection( const QPointF& pos, + qreal startAngle, qreal spanAngle, qreal apectRatio ) noexcept + : QskConicDirection( pos.x(), pos.y(), startAngle, spanAngle, apectRatio ) { } @@ -345,6 +353,11 @@ inline constexpr qreal QskConicDirection::y() const noexcept return m_y; } +inline constexpr qreal QskConicDirection::aspectRatio() const noexcept +{ + return m_aspectRatio; +} + inline constexpr QskRadialDirection::QskRadialDirection( qreal x, qreal y, qreal radius ) noexcept : m_x( x ) diff --git a/src/nodes/QskGradientMaterial.cpp b/src/nodes/QskGradientMaterial.cpp index 164d9f15..7be7d994 100644 --- a/src/nodes/QskGradientMaterial.cpp +++ b/src/nodes/QskGradientMaterial.cpp @@ -510,6 +510,16 @@ namespace const auto dir = gradient.conicDirection(); + float ratio = dir.aspectRatio(); + if ( ratio <= 0.0f ) + ratio = 1.0f; + + if ( ratio != m_aspectRatio ) + { + m_aspectRatio = ratio; + changed = true; + } + const QVector2D center( dir.x(), dir.y() ); if ( center != m_center ) @@ -555,6 +565,7 @@ namespace const auto mat = static_cast< const ConicMaterial* >( other ); if ( ( m_center != mat->m_center ) + || !qskFuzzyCompare( m_aspectRatio, mat->m_aspectRatio ) || !qskFuzzyCompare( m_start, mat->m_start ) || !qskFuzzyCompare( m_span, mat->m_span ) ) { @@ -567,6 +578,7 @@ namespace QSGMaterialShader* createShader() const override; QVector2D m_center; + float m_aspectRatio = 1.0; float m_start = 0.0; float m_span = 1.0; }; @@ -585,6 +597,7 @@ namespace GradientShaderGL::initialize(); m_centerCoordId = program()->uniformLocation( "centerCoord" ); + m_aspectRatioId = program()->uniformLocation( "aspectRatio" ); m_startId = program()->uniformLocation( "start" ); m_spanId = program()->uniformLocation( "span" ); } @@ -594,12 +607,14 @@ namespace auto material = static_cast< const ConicMaterial* >( newMaterial ); program()->setUniformValue( m_centerCoordId, material->m_center ); + program()->setUniformValue( m_aspectRatioId, material->m_aspectRatio ); program()->setUniformValue( m_startId, material->m_start ); program()->setUniformValue( m_spanId, material->m_span ); } private: int m_centerCoordId = -1; + int m_aspectRatioId = -1; int m_startId = -1; int m_spanId = -1; }; @@ -620,7 +635,7 @@ namespace auto matNew = static_cast< ConicMaterial* >( newMaterial ); auto matOld = static_cast< ConicMaterial* >( oldMaterial ); - Q_ASSERT( state.uniformData()->size() >= 84 ); + Q_ASSERT( state.uniformData()->size() >= 88 ); auto data = state.uniformData()->data(); bool changed = false; @@ -639,22 +654,28 @@ namespace changed = true; } + if ( matOld == nullptr || matNew->m_aspectRatio != matOld->m_aspectRatio ) + { + memcpy( data + 72, &matNew->m_aspectRatio, 4 ); + changed = true; + } + if ( matOld == nullptr || matNew->m_start != matOld->m_start ) { - memcpy( data + 72, &matNew->m_start, 4 ); + memcpy( data + 76, &matNew->m_start, 4 ); changed = true; } if ( matOld == nullptr || matNew->m_span != matOld->m_span ) { - memcpy( data + 76, &matNew->m_span, 4 ); + memcpy( data + 80, &matNew->m_span, 4 ); changed = true; } if ( state.isOpacityDirty() ) { const float opacity = state.opacity(); - memcpy( data + 80, &opacity, 4 ); + memcpy( data + 84, &opacity, 4 ); changed = true; } diff --git a/src/nodes/shaders/gradientconic-vulkan.frag b/src/nodes/shaders/gradientconic-vulkan.frag index 526e224d..329d4e4c 100644 --- a/src/nodes/shaders/gradientconic-vulkan.frag +++ b/src/nodes/shaders/gradientconic-vulkan.frag @@ -7,6 +7,7 @@ layout( std140, binding = 0 ) uniform buf { mat4 matrix; vec2 centerCoord; + float aspectRatio; float start; float span; float opacity; diff --git a/src/nodes/shaders/gradientconic-vulkan.vert b/src/nodes/shaders/gradientconic-vulkan.vert index c23fdebb..31c26b26 100644 --- a/src/nodes/shaders/gradientconic-vulkan.vert +++ b/src/nodes/shaders/gradientconic-vulkan.vert @@ -7,6 +7,7 @@ layout( std140, binding = 0 ) uniform buf { mat4 matrix; vec2 centerCoord; + float aspectRatio; float start; float span; float opacity; @@ -17,5 +18,7 @@ out gl_PerVertex { vec4 gl_Position; }; void main() { coord = vertexCoord.xy - ubuf.centerCoord; + coord.y *= ubuf.aspectRatio; + gl_Position = ubuf.matrix * vertexCoord; } diff --git a/src/nodes/shaders/gradientconic.frag.qsb b/src/nodes/shaders/gradientconic.frag.qsb index 3e75c1acad875cce2d6ca9390e97b38118d41623..c2a0848e801c75fc6256450d497ac25b062606f3 100644 GIT binary patch literal 1830 zcmV+>2if=l02nxUob6caPa8)NU%))-kfccy+B7A-LX(^m;~2+)!z)memxKfYlte=4 zbezxTTKmqrTN7v$^+Q#upZc-?Q~!qkBmL6H%+X9>&0Nm-zs9L7?5V0l*IUu?v?0Fz6 z{d}r|NnU9X;s6feQ$p8If{qCNHn=8%np#h2* zGb#Ojr-{GqHALSPjkP$?KRJy0(-7qo;$DJ;qs4O|g5OWpx7TwKujGm|c^T$F?Ds`B zEP}(kVSAV&b`$Or!<`)v{9)7qPq@<)WJA%PF||)AcAeG+1w9zklk~0ghMBA!#p@&7 zipJ|F-cyY?K)iL0mm=P##v3Hwj_`FL)`xzNC*l~VI24WX-_YoiMt=ZW`UQSK_=3-4 z@PwOVh&v@>8>S8W5a(o-jAaQ#k!bd!LZ(b+U)_IPtCmeS>1}Bl}2yO8QaW zAgt2&IbpAmKBR9F5B1$5eJUQLZ__%;n@r8H_IfrEKNkAW7k^q&{uG$39h9G==Ml1x zbcuKhUuLp~X5q7+wFdxVegV!P$;v&Hd#UJEqE`WBFj-sUl#?a$Z-V%vKp{P;#WX>( z7-xoZb6baL>AdhR)w4T>uxKA+@G@V7_ ze@k*RB#Zp-h&M~I;o34}?;7!xjtAs}qUVWLd08MoM`%siS|r&8&Bvu4d0r;I()W<` zrD#pXyh1u3lAo)@djRWb`wJ%Deyf_jHL_RM>^&m>4CBY zaW>2W4(lgPwKhunvEMH-+3U*QWx{QO9_**9#KZo7LAu9C4%dDIeMpz&bVw zk<&jndFY0b7x>0>KA&!euHy;nz^ffJjcPrx`9c@l^MRR;c-V5dvEQnuU#AVY8a6ND zN9Y|b2*#o76b#4pxf_;)AgoBPP-@sw({=ck&AmWpN8Ap12cv2GIvq4^$K%Hf={Mj) z%lE25*gzkQ7JO(d>1kv)nsqlUtQk&F55g@V0dr`F$DUs!|D8cRmx)qE3O&S8KV zawc7+?jVd6+>}_I{+fPDr#WIZx z|6seM3i*F}nfzZM@1{V0)C%N$yFh0Cr3K>o9kmg(xFO1;3Rx;6?#%V_AF7r(31=;o zemZy1mD+FsTy;aSFY}PFnioM0nEUxShruhWVSfpxgrCKuqA*~|>H!%r^i@!;MlLs< zydF|P0=242po<*|5MZs=lHlCe-Efh{F5s&kK*(JJH`9%xq$Pp5Zt5>W@qhFPj7EN+j?%5!yt;RXMVHA z^};1JU~A{m%ksv?)>>W$(nENo#S_w+JD zr04f)_TAl91Vg1BIKD0?=7{IlJs-xAWpfC4zr|h4f_ACDvX3K6E@m>x*5(JHYgd5u zBQ8zdGN@72Nvt9VqBak+>b2fxZ37*iI2_qn%3^W%oNe$Sdb`s^2sZs;6LMl{m z?CoV`0Jbj*;Fjn`0La&$$na_508*VIB(!&QXi+W*SSCJhDIf7(GG4z~c#G48{GZUB zpQmx=`MG$a#n12Jv=JmmLC0Ln>U*Bp>#oCyw)24;7&tKoMSkdXk~pErU+wYg@%wLL U#GHy{to>`nXoAoF0C7n|7(Wh=0RR91 literal 1803 zcmV+m2lV&=02d~Bob6a!Pa8)R9>4^;AxV=aY15Q+3c0Kk;~2*f2L}Q*xk*StfRaiP zS}p6f+1B2*cExPL0csd-_A6bi`$F^?Rt2QXYOD-0bt$KGy-DgA+hHw`WNaLM z_4Ag2LXRd`12hZJ7_fbykJH$)fqPp&oCmjk76RM{fpSj>%hJ?<%VG(LxBwO8+av9N z?==q4CO|_1t{nzE38oR~f!~515PbbGxS5zUGWjZv8=#^AiWsvgjYDq|r|THuzBLs8 zIKAh`Nf3p6b}naGiHKXC;5)6V_0r;SHC$1^e{ry1l8imy%X7~Ug&&urD6S}OwH~>G zC&Gs86JT47L);szSyE%;}!61UN;`*D7idr>`#H-LoEASCO` zZqyP;7kD*xYh}a%Mlz((im;W@J-6QS(-cqai&h+R;qSw*Z@1NFq-tAYGd!2grPJUg z=!=31#28`?9{!|XvuxtiTi^P7A=n=js)EzT8+ z`CNW(wlH5P7H-|1OY<47bYSw*X11Dc>^8uTy$2XzMX|k?Mpf=kV%Q}Sodgij@k<5h zmjri`Z&_Gy7!?)XM5iKB)^rn8HC*o1-LS?hfm;isLrNsn^rmk`-TpFVC&R6cr z;U#X=d6zUbT4#I{=(wj@+JjjIx4Y393b(&`FmVl-pH5=cldaPQ@k4qb7d0QK?d^J z{rq>dJ@}eEwDmLQy!T9T->P6tI4ISt#HdUQQ?JUru}pAJ&5B&U7eKNWB+!WVPOe=& z(F>>HehO~2`xh-bs)<1cEPobNtBEgcFPHA>Y6&!2y9Zt9N|*%8Jodt}pAP^+^7?Qu z`f-$)9_Uh=IT*9-?ftuSzb13T>uf$H$6k~ufB^pUWflN-<8e`O+F}d9eUYT1A+;r|6o6a9P zo6h{debc$;#s|1fF?XIv+-2rh|3mxCTHSd7^S=rXJkM<(b+d_;Q0aWG4D(JU#ud1W3=a=RB!3Kbz!M%OOpy&u zf5h~$K#Eqa?MdE3EnQ}7^wKU8+M^K3Q2{@x9tL9J5qo!Ag zUI7$ms;(v}CyV6Y6!9m3LVDVWX^Lbq&NSubnjw3g_+JoxgLqnIhGa5^OqTc);2F|4 z$=(H$$MLU09>->BJZHpki(<$dIt#@AlH}${7WrQhZ=Pbqu_egf1>$KPcgP1#FA%Ns zQX)UcX-wO?OR^=y$Hf78ULwBMcaQX?XiUevOgit8pDV<>1LJ7>W2O#LR}6crWUp-4 zyHEUYNbUj2-Y309;{6O>6ycsHoIa$*b>TMPuzoD6wF%OXy?&ahKG*in5N;jxU_YHB t9`^n-(tV!faO@Y*hjdY`Ec*SSOWZi@QfJtB4LC;a`Qkks`x>t#6aGhUhlv0H diff --git a/src/nodes/shaders/gradientconic.vert b/src/nodes/shaders/gradientconic.vert index 53b3dbf4..ab2834b8 100644 --- a/src/nodes/shaders/gradientconic.vert +++ b/src/nodes/shaders/gradientconic.vert @@ -1,6 +1,7 @@ attribute vec4 vertexCoord; uniform mat4 matrix; +uniform highp float aspectRatio; uniform vec2 centerCoord; varying vec2 coord; @@ -8,5 +9,7 @@ varying vec2 coord; void main() { coord = vertexCoord.xy - centerCoord; + coord.y *= aspectRatio; + gl_Position = matrix * vertexCoord; } diff --git a/src/nodes/shaders/gradientconic.vert.qsb b/src/nodes/shaders/gradientconic.vert.qsb index abf4cd74b00bf5e79a4a854f0345324b631f025f..e1b0505ce22435643e08ffde070d5f92dd807372 100644 GIT binary patch literal 1617 zcmV-X2Cn%403?Zcob6g$Z`4*0KH1&e93b41mKKU36n43oCEXB28U#wK1}RdafDlp? zS&rA6y4|&deVhv@PgURg(idL(8~OwKWBNb(QnfSZjO}B4H-uJAA;ieLo;lxK&&>F& z2xDxB$*TnSINM|n3mAvbZ8pbz_!qDytW*&47hNoOon|I`0YWChQ3Ydta1Ztp!9|VP ziiOf%77#wb)>y<^3|)?r=jRG)0PbU~!z}P6U@nBf*a)pXRB$KbhjH-0VV;OL$i!PB zks;RQ8V3s@+W;XOM3;p<7eu9>^KrWJmj*%{z?feNUFZcJ68bH0O#+vC0Z)Kg1lr&L zXFCLc+6(?quRi)dM&HeJsq#~~G%<(}4cxv>+*6Q26dHj|!S8Pto-7z4ZyJrMy7ar8 zC?2IOqsJY~#hBSJ8M}(Iu!8hwq#NmD3Ox^Wyrunu?oa561e5Z>S@9h6`Uv+>;gyIt zukgymTTplv;w>q>e&Rh9zLpbx==W*(zO2N7^d-f|EA%{)V9J7r@@L?)B=BY7i|p94 zD0?05k3b*BHb84S&_m=m@@I%1CLZE7!dD1C!s6bP_z~I%jmB)0+V$T>$^y? z4+y@@tww8~)7lmC73r(Qi)~*c-H5M}?NOp{5f=5`g}p4XvD&aM+b&c_YP$Il>x^i9It zAU&90gZAVa*)a(BCuHlM98;BehOl%_!9xF%I;ZZ@!rRTMVe#B+PNBfjtV-qU6gS3j zG`rrP*@g6N79YB#KBVhPtmxaMq95tIr1NO@9qjCjeZ50_^av>Q>k9=xso)y47RNM2 zvJFLcT9Lg=_NEEjW!56mWXUmP) zo)L^q$DY(}$Ky^g=lelZa$9ZR;<^=XI5uClxa+IzkXr%IFg7etrTq=dcKObXwmYL~ zk>|Gjpo2c>5qtn^(+YN6Z=GyQs}={J)*|j8w*emkcecqRZPRy~I!0WrQ5-@_pH*?j z_Kto_m@o?Q2)MDMU!RqLLS*Z0bIA`~j+rBj%#waR>&vdD^+-S5eZL;Y^rvf5zxjfj zu;2vm*3RU>#!G-Ft^!zH+v>KF+bp@%KAJo<8~HBnR`2xZ`snba|3m4c*GeBfPd9Sv zo!v-3;+ai!z4xwpUMtP>%c03RFb{95l<0IwSNS&^47ETGvvgk%oOL$@$E$1gIL^S| zGCS6Heju~b*lq~tOmgnN4mtncZ?!^)SM7RD*^WsGR8o;cH&nC)D{0Atjh98ZsE7h^ zs|ye+C5M|TqNslIKx3ySOA5*wJhLeiYNls0OG$xc@!8g<7J0Jtc=?$*7x-alKK3>u zPI)iNfLl+$em=LjxcpUJ`qM?lV#ISg$eFCPVnHpu`}&h-N=&$HdQ~8^3 zDF05>t}2>uswGzwHC>EZwr3%oI5pNC)5Yi5c{QoCD!?doni9EWbTKRy_=1oim1AT> z%M?uk8>iJyJqR2g1zyg`Zm*_>SzMVvYU=-~snw;-`{Q3Wdm2(Y=~KY;hdmA6{)Ko@ z!z1+~3sfrOeVH*8mi~Vrj-1^I*R$JE)6u`|INWMO*EZYk+5;`+hi<2N8$XP1AHZ=e z#Gfsm)p4M0ZATYUGhI%K$wX35i*fD#hv!9d1mmq^4!U*C%1RW%AZ`1$SFf$E_UxRM zl?{4CP(i!v_3rzXmEsb?DkqxQPW5D+w3YM-cf*4UsHL=*1jq~_k%9^+YLs4Sd%*&7S?lfeb4o7@{DZc P^~6_RUR-|zA(JGiM*lJj literal 1503 zcmV<51t9tW03YLcob6fNPuxZj-}~UpkdTm&HcirQLP+oUxLgr56%-Uis}czb6r!k7 zge>FpQMY&Y#UF4a;;HIeU;5JbzV%PcOV!Toj?ZiRAf%;15FGh#XZANAJF~Yt6BuJX zOg?#dZ?IivF^>iC*=8%ug?}Ea!Aub$f78Q~&j{1mOOVkCjT#us!P|F;1kP#9RP^VM z(2wX|w#`CThfzO#N8@J-3fD2jn#=%CJZ6Lcj1AJ9 z!kWduZd!rS(EXs+u(y}AnD5)o+C%)P{IH~Hp>I1o+%cM#-!e>#3#kQ7^8+t51HK*B zwbvSlr`~XlV75KWn?~T-?S+^*!%fQxEN{hiy_&-3eqeY(g3&S@m3G0T9qcb?2eHMb zVLMgb4FlCKm_X(H#f#9lyi&t8ooab&OVKsUcN+SJ>)V0tI)H1@5J4I3wv$*C-?hBJ zYOjz9BY*{Nhk;c}+2H!iKnINFB~XvbkrAmUUVgAoLuZg&=7fY}6jH9NPLd`i3_bj)7etUHYP0?Xm}(m@IYw z?ONsu?Y_{l>#o;CVpbd3`6dwiVa%s3%)r=Jkq`)V@a=akI?FwboG8T}q8{Ka1j`LRA6 zYH-0kq=~C=XAQ+QH)%ZB!4w29k{_ovqQBRGn-}s0;fJh{3J_t&IJ_SL9&x=i$ARuApOL>n z^Z@Y?uMxgT_(2v`LlQqoc|hLBKo8SgQTWv-);di3dzBY;T_r!RkS?T0h&KxKHS!yE zPZE8dXp|o%Y@e_tu`W|5;efL>Z*GYXyj}s4l z{hWMdq#x-L=|K51jeC{(X|nkxKO=tR6DOa30~w_65{8o= zjNur?{WjSdBOL0$SUjLOE(3)zno#g11vg1^zkpt(r$}~Ek*z4Q(`2tg*uNk`Ux_Gn z&M6G}OdP(#8&lRS;;^2F8-K_4;ruLZSEJ+BbNBS7@uTa-btE35Ivt|(9FG2<+2rO>bA4Zb;RyX?SD&oI=lSMyB^U?MRg7-x$G?yDx7^3guu)P3r_{Yw{x^4WHUhGG$V8t< z)jW)&rKmYrQ~}wlI`$$7R1zO)q;N?_7yqQVD+mcvIjMR-u|+f3C>LhzC>8NZBM0fy zvCTS9ZZa+%XHLI#eAin3Jywj#5>RdE@uXe8Xy{M8p+D!X`Y{{s)Q|KGHr%K7BURgE zB>CBElTR&njKw`JJcU2i9``t`$|9N{@kNY9^w-^2F|I^QYi2deU`ag_