QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskArcRenderer.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskArcRenderer.h"
7#include "QskArcMetrics.h"
8#include "QskGradient.h"
9#include "QskVertex.h"
10#include "QskVertexHelper.h"
11#include "QskRgbValue.h"
12
13#include <qsggeometry.h>
14
15static inline QskVertex::Line* qskAllocateLines(
16 QSGGeometry& geometry, int lineCount )
17{
18 geometry.allocate( 2 * lineCount ); // 2 points per line
19 return reinterpret_cast< QskVertex::Line* >( geometry.vertexData() );
20}
21
22static inline QskVertex::ColoredLine* qskAllocateColoredLines(
23 QSGGeometry& geometry, int lineCount )
24{
25 geometry.allocate( 2 * lineCount ); // 2 points per line
26 return reinterpret_cast< QskVertex::ColoredLine* >( geometry.vertexData() );
27}
28
29namespace
30{
31 template< class Line >
32 class OrthogonalStroker
33 {
34 public:
35 OrthogonalStroker( const QRectF& rect, qreal thickness, qreal border )
36 : m_thickness( thickness )
37 , m_border( border )
38 , m_rx( 0.5 * ( rect.width() - m_thickness ) )
39 , m_ry( 0.5 * ( rect.height() - m_thickness ) )
40 , m_offsetToBorder( 0.5 * m_thickness - border )
41 , m_aspectRatio( m_rx / m_ry )
42 , m_cx( rect.x() + 0.5 * rect.width() )
43 , m_cy( rect.y() + 0.5 * rect.height() )
44 {
45 }
46
47 inline void setLinesAt( const qreal radians,
48 const QskVertex::Color fillColor, const QskVertex::Color borderColor,
49 Line* fill, Line* outerBorder, Line* innerBorder ) const
50 {
51 const auto cos = qFastCos( radians );
52 const auto sin = qFastSin( radians );
53
54 const auto v = normalVector( cos, sin );
55
56 const QPointF p0( m_cx + m_rx * cos, m_cy - m_ry * sin );
57
58 const auto v1 = v * m_offsetToBorder;
59
60 const auto p1 = p0 + v1;
61 const auto p2 = p0 - v1;
62
63 if ( fill )
64 fill->setLine( p1, p2, fillColor );
65
66 if ( outerBorder )
67 {
68 const auto v2 = v * m_border;
69
70 outerBorder->setLine( p1 + v2, p1, borderColor );
71 innerBorder->setLine( p2 - v2, p2, borderColor );
72 }
73 }
74
75 inline void setClosingBorderLines( const Line& l,
76 Line* lines, qreal sign, const QskVertex::Color color ) const
77 {
78 const auto& pos = l.p1;
79 const auto& l0 = lines[0];
80
81 const auto dx = sign * l0.dy();
82 const auto dy = sign * l0.dx();
83
84 lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color );
85 lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color );
86 lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color );
87 }
88
89 private:
90 inline QPointF normalVector( const qreal cos, const qreal sin ) const
91 {
92 /*
93 The inner/outer points are found by shifting orthogonally along the
94 ellipse tangent:
95
96 m = w / h * tan( angle )
97 y = m * x;
98 x² + y² = 1.0
99
100 => x = 1.0 / sqrt( 1.0 + m² );
101
102 Note: the angle of the orthogonal vector could
103 also be found ( first quadrant ) by:
104
105 atan2( tan( angle ), h / w );
106
107 Note: we return the vector mirrored vertically, so that it
108 matches the coordinate system used by Qt.
109 */
110
111 if ( qFuzzyIsNull( cos ) )
112 return { 0.0, ( sin < 0.0 ) ? 1.0 : -1.0 };
113
114 const qreal m = m_aspectRatio * ( sin / cos );
115 const qreal t = 1.0 / qSqrt( 1.0 + m * m );
116
117 const auto dx = ( cos >= 0.0 ) ? t : -t;
118 return { dx, -m * dx };
119 }
120
121 const qreal m_thickness;
122 const qreal m_border;
123
124 // radii t the middle of the arc
125 const qreal m_rx, m_ry;
126
127 // distances between the middle and the beginning of the border
128 const qreal m_offsetToBorder;
129
130 const qreal m_aspectRatio; // m_rx / m_ry
131
132 // center
133 const qreal m_cx, m_cy;
134 };
135
136 template< class Line >
137 class RadialStroker
138 {
139 public:
140 RadialStroker( const QRectF& rect, qreal thickness, qreal border )
141 : m_sx( qMax( rect.width() / rect.height(), 1.0 ) )
142 , m_sy( qMax( rect.height() / rect.width(), 1.0 ) )
143 , m_rx1( 0.5 * rect.width() )
144 , m_ry1( 0.5 * rect.height() )
145 , m_rx2( m_rx1 - m_sx * border )
146 , m_ry2( m_ry1 - m_sy * border )
147 , m_rx3( m_rx1 - m_sx * ( thickness - border ) )
148 , m_ry3( m_ry1 - m_sy * ( thickness - border ) )
149 , m_rx4( m_rx1 - m_sx * thickness )
150 , m_ry4( m_ry1 - m_sy * thickness )
151 , m_center( rect.x() + m_rx1, rect.y() + m_ry1 )
152 {
153 }
154
155 inline void setLinesAt( const qreal radians,
156 const QskVertex::Color fillColor, const QskVertex::Color borderColor,
157 Line* fill, Line* outer, Line* inner ) const
158 {
159 const QPointF v( qFastCos( radians ), -qFastSin( radians ) );
160
161 const auto x1 = m_center.x() + m_rx2 * v.x();
162 const auto y1 = m_center.y() + m_ry2 * v.y();
163
164 const auto x2 = m_center.x() + m_rx3 * v.x();
165 const auto y2 = m_center.y() + m_ry3 * v.y();
166
167 if ( fill )
168 fill->setLine( x1, y1, x2, y2, fillColor );
169
170 if ( outer )
171 {
172 const auto x3 = m_center.x() + m_rx1 * v.x();
173 const auto y3 = m_center.y() + m_ry1 * v.y();
174
175 const auto x4 = m_center.x() + m_rx4 * v.x();
176 const auto y4 = m_center.y() + m_ry4 * v.y();
177
178 outer->setLine( x3, y3, x1, y1, borderColor );
179 inner->setLine( x4, y4, x2, y2, borderColor );
180 }
181 }
182
183 inline void setClosingBorderLines( const Line& l,
184 Line* lines, qreal sign, const QskVertex::Color color ) const
185 {
186 const auto& pos = l.p1;
187
188 // Good enough until it is decided if we want to keep the radial mode.
189 const auto& l0 = lines[0];
190
191 const auto s = m_sx / m_sy;
192 const auto dx = sign * l0.dy() * s;
193 const auto dy = sign * l0.dx() / s;
194
195 lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color );
196 lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color );
197 lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color );
198 }
199
200 private:
201 // stretch factors of the ellipse
202 const qreal m_sx, m_sy;
203
204 // radii: out->in
205 const qreal m_rx1, m_ry1, m_rx2, m_ry2, m_rx3, m_ry3, m_rx4, m_ry4;
206
207 // center point
208 const QPointF m_center;
209 };
210
211 template< class Line >
212 class CircularStroker
213 {
214 public:
215 CircularStroker( const QRectF& rect, qreal thickness, qreal border )
216 : m_center( rect.center() )
217 , m_radius( 0.5 * ( rect.width() - thickness ) )
218 , m_distOut( 0.5 * thickness )
219 , m_distIn( m_distOut - border )
220 {
221 }
222
223 inline void setLinesAt( const qreal radians,
224 const QskVertex::Color fillColor, const QskVertex::Color borderColor,
225 Line* fill, Line* outer, Line* inner ) const
226 {
227 const QPointF v( qFastCos( radians ), -qFastSin( radians ) );
228
229 const auto p0 = m_center + m_radius * v;
230 const auto dv1 = v * m_distIn;
231
232 const auto p1 = p0 + dv1;
233 const auto p2 = p0 - dv1;
234
235 if ( fill )
236 fill->setLine( p1, p2, fillColor );
237
238 if ( outer )
239 {
240 const auto dv2 = v * m_distOut;
241
242 const auto p3 = p0 + dv2;
243 const auto p4 = p0 - dv2;
244
245 outer->setLine( p3, p1, borderColor );
246 inner->setLine( p4, p2, borderColor );
247 }
248 }
249
250 inline void setClosingBorderLines( const Line& l,
251 Line* lines, qreal sign, const QskVertex::Color color ) const
252 {
253 const auto& pos = l.p1;
254 const auto& l0 = lines[0];
255
256 const auto dx = sign * l0.dy();
257 const auto dy = sign * l0.dx();
258
259 lines[-3].setLine( pos.x, pos.y, pos.x, pos.y, color );
260 lines[-2].setLine( pos.x + dx, pos.y - dy, pos.x, pos.y, color );
261 lines[-1].setLine( l0.x1() + dx, l0.y1() - dy, l0.x1(), l0.y1(), color );
262 }
263
264 private:
265 // center point
266 const QPointF m_center;
267 const qreal m_radius; // middle of the arc
268
269 // distances from the middle to the inner/outer side of the border
270 const qreal m_distOut, m_distIn;
271 };
272}
273
274namespace
275{
276 class Renderer
277 {
278 public:
279 Renderer( const QRectF&, const QskArcMetrics&,
280 bool radial, const QskGradient&, const QskVertex::Color& );
281
282 int fillCount() const;
283 int borderCount() const;
284
285 template< class Line >
286 void renderArc( const qreal thickness, const qreal border, Line*, Line* ) const;
287
288 private:
289 int arcLineCount() const;
290
291 template< class LineStroker, class Line >
292 void renderLines( const LineStroker&, Line*, Line* ) const;
293
294 const QRectF& m_rect;
295
296 const qreal m_radians1;
297 const qreal m_radians2;
298
299 const bool m_radial; // for circular arcs radial/orthogonal does not differ
300 const bool m_closed;
301
302 const QskGradient& m_gradient;
303 const QskVertex::Color m_borderColor;
304 };
305
306 Renderer::Renderer( const QRectF& rect, const QskArcMetrics& metrics,
307 bool radial, const QskGradient& gradient, const QskVertex::Color& borderColor )
308 : m_rect( rect )
309 , m_radians1( qDegreesToRadians( metrics.startAngle() ) )
310 , m_radians2( qDegreesToRadians( metrics.endAngle() ) )
311 , m_radial( radial )
312 , m_closed( metrics.isClosed() )
313 , m_gradient( gradient )
314 , m_borderColor( borderColor )
315 {
316 }
317
318 int Renderer::arcLineCount() const
319 {
320 // not very sophisticated - TODO ...
321
322 const auto radius = 0.5 * qMax( m_rect.width(), m_rect.height() );
323 const auto radians = qAbs( m_radians2 - m_radians1 );
324
325 const auto count = qCeil( ( radius * radians ) / 3.0 );
326 return qBound( 3, count, 160 );
327 }
328
329 int Renderer::fillCount() const
330 {
331 if ( !m_gradient.isVisible() )
332 return 0;
333
334 return arcLineCount() + m_gradient.stepCount() - 1;
335 }
336
337 template< class Line >
338 void Renderer::renderArc( const qreal thickness, const qreal border,
339 Line* fillLines, Line* borderLines ) const
340 {
341 if ( qskFuzzyCompare( m_rect.width(), m_rect.height() ) )
342 {
343 const CircularStroker< Line > stroker( m_rect, thickness, border );
344 renderLines( stroker, fillLines, borderLines );
345 }
346 else if ( m_radial )
347 {
348 const RadialStroker< Line > stroker( m_rect, thickness, border );
349 renderLines( stroker, fillLines, borderLines );
350 }
351 else
352 {
353 const OrthogonalStroker< Line > stroker( m_rect, thickness, border );
354 renderLines( stroker, fillLines, borderLines );
355 }
356 }
357
358 template< class LineStroker, class Line >
359 void Renderer::renderLines( const LineStroker& lineStroker,
360 Line* fillLines, Line* borderLines ) const
361 {
362 QskVertex::GradientIterator it;
363
364 if ( fillLines )
365 {
366 if ( m_gradient.stepCount() <= 1 )
367 {
368 it.reset( m_gradient.rgbStart(), m_gradient.rgbEnd() );
369 }
370 else
371 {
372 it.reset( m_gradient.stops() );
373 it.advance(); // the first stop is always covered by the contour
374 }
375 }
376
377 const auto count = arcLineCount();
378 const auto radiansSpan = m_radians2 - m_radians1;
379
380 const qreal stepMax = count - 1;
381 const auto stepSize = radiansSpan / stepMax;
382
383 auto l = fillLines;
384
385 auto outer = borderLines;
386 auto inner = borderLines;
387
388 if ( borderLines )
389 {
390 outer = borderLines;
391 if ( !m_closed )
392 outer += 3;
393
394 inner = outer + count;
395 if ( !m_closed )
396 inner += 3;
397 }
398
399 for ( int i = 0; i < count; i++ )
400 {
401 const auto progress = i / stepMax;
402
403 while( !it.isDone() && ( it.position() < progress ) )
404 {
405 const auto radians = m_radians1 + it.position() * radiansSpan;
406 lineStroker.setLinesAt( radians, it.color(), m_borderColor,
407 l++, nullptr, nullptr );
408
409 it.advance();
410 }
411
412 const auto radians = m_radians1 + i * stepSize;
413 const auto color = it.colorAt( progress );
414
415 lineStroker.setLinesAt( radians, color, m_borderColor,
416 l ? l++ : nullptr,
417 outer ? outer + i : nullptr,
418 inner ? inner + count - 1 - i : nullptr
419 );
420 }
421
422 if ( borderLines && !m_closed )
423 {
424 const auto sign = ( radiansSpan > 0.0 ) ? 1.0 : -1.0;
425
426 lineStroker.setClosingBorderLines( inner[count - 1], outer, sign, m_borderColor );
427 lineStroker.setClosingBorderLines( outer[count - 1], inner, sign, m_borderColor );
428 }
429 }
430
431 int Renderer::borderCount() const
432 {
433 if ( m_borderColor.a == 0 )
434 return 0;
435
436 auto count = 2 * arcLineCount();
437 if ( !m_closed )
438 count += 2 * 3;
439
440 return count;
441 }
442}
443
444bool QskArcRenderer::isGradientSupported( const QRectF& rect,
445 const QskArcMetrics& metrics, const QskGradient& gradient )
446{
447 if ( rect.isEmpty() || metrics.isNull() )
448 return true;
449
450 if ( !gradient.isVisible() || gradient.isMonochrome() )
451 return true;
452
453 switch( gradient.type() )
454 {
455 case QskGradient::Stops:
456 {
457 return true;
458 }
459 case QskGradient::Conic:
460 {
461#if 0
462 const auto direction = gradient.conicDirection();
463 if ( direction.center() == rect.center() )
464 {
465 const auto aspectRatio = rect.width() / rect.height();
466 if ( qskFuzzyCompare( direction.aspectRatio(), aspectRatio ) )
467 {
468 /*
469 we should be able to create a list of stops from
470 this gradient that works for the renderer. TODO ...
471 */
472 }
473 }
474#endif
475
476 return false;
477 }
478 default:
479 {
480 return false;
481 }
482 }
483
484 return false;
485}
486
487void QskArcRenderer::setColoredBorderLines( const QRectF& rect,
488 const QskArcMetrics& metrics, bool radial, qreal borderWidth,
489 const QColor& borderColor, QSGGeometry& geometry )
490{
491 geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
492 geometry.markVertexDataDirty();
493
494 if ( borderWidth <= 0.0 || !QskRgb::isVisible( borderColor ) )
495 {
496 qskAllocateColoredLines( geometry, 0 );
497 return;
498 }
499
500 const Renderer renderer( rect, metrics, radial, QskGradient(), borderColor );
501
502 if ( const auto lines = qskAllocateColoredLines( geometry, renderer.borderCount() ) )
503 {
504 renderer.renderArc( metrics.thickness(), borderWidth,
505 static_cast< QskVertex::ColoredLine* >( nullptr ), lines );
506 }
507}
508
509void QskArcRenderer::setColoredFillLines( const QRectF& rect, const QskArcMetrics& metrics,
510 bool radial, qreal borderWidth, const QskGradient& gradient, QSGGeometry& geometry )
511{
512 geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
513 geometry.markVertexDataDirty();
514
515 if ( !gradient.isVisible() )
516 {
517 qskAllocateColoredLines( geometry, 0 );
518 return;
519 }
520
521 const Renderer renderer( rect, metrics, radial, gradient, QColor( 0, 0, 0, 0 ) );
522
523 if ( const auto lines = qskAllocateColoredLines( geometry, renderer.fillCount() ) )
524 {
525 renderer.renderArc( metrics.thickness(), borderWidth, lines,
526 static_cast< QskVertex::ColoredLine* >( nullptr ) );
527 }
528}
529
530void QskArcRenderer::setColoredBorderAndFillLines( const QRectF& rect,
531 const QskArcMetrics& metrics, bool radial, qreal borderWidth,
532 const QColor& borderColor, const QskGradient& gradient, QSGGeometry& geometry )
533{
534 geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
535 geometry.markVertexDataDirty();
536
537 const Renderer renderer( rect, metrics, radial, gradient,
538 borderColor.isValid() ? borderColor : QColor( 0, 0, 0, 0 ) );
539
540 const auto borderCount = renderer.borderCount();
541 const auto fillCount = renderer.fillCount();
542
543 auto lineCount = borderCount + fillCount;
544 if ( borderCount && fillCount )
545 lineCount++; // connecting line
546
547 const auto lines = qskAllocateColoredLines( geometry, lineCount );
548 if ( lines )
549 {
550 const auto fillLines = fillCount ? lines : nullptr;
551 const auto borderLines = borderCount ? lines + lineCount - borderCount : nullptr;
552
553 renderer.renderArc( metrics.thickness(), borderWidth, fillLines, borderLines );
554
555 if ( fillCount && borderCount )
556 {
557 const auto idx = fillCount;
558
559 lines[idx].p1 = lines[idx - 1].p2;
560 lines[idx].p2 = lines[idx + 1].p1;
561 }
562 }
563}
564
565void QskArcRenderer::setBorderLines( const QRectF& rect,
566 const QskArcMetrics& metrics, bool radial, qreal borderWidth, QSGGeometry& geometry )
567{
568 geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
569 geometry.markVertexDataDirty();
570
571 if ( borderWidth <= 0.0 )
572 {
573 qskAllocateLines( geometry, 0 );
574 return;
575 }
576
577 const Renderer renderer( rect, metrics, radial, QskGradient(), QskRgb::Black );
578
579 const auto lines = qskAllocateLines( geometry, renderer.borderCount() );
580 if ( lines )
581 {
582 QskVertex::Line* fill = nullptr;
583 renderer.renderArc( metrics.thickness(), borderWidth, fill, lines );
584 }
585}
586
587void QskArcRenderer::setFillLines( const QRectF& rect,
588 const QskArcMetrics& metrics, bool radial, qreal borderWidth, QSGGeometry& geometry )
589{
590 geometry.setDrawingMode( QSGGeometry::DrawTriangleStrip );
591 geometry.markVertexDataDirty();
592
593 const Renderer renderer( rect, metrics, radial, QskRgb::Black, 0 );
594
595 const auto lines = qskAllocateLines( geometry, renderer.fillCount() );
596 if ( lines )
597 {
598 QskVertex::Line* border = nullptr;
599 renderer.renderArc( metrics.thickness(), borderWidth, lines, border );
600 }
601}