FifeGUI 0.2.0
A C++ GUI library designed for games.
backends/sdl3/graphics.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
2// SPDX-FileCopyrightText: 2004 - 2008 Olof Naessén and Per Larsson
3// SPDX-FileCopyrightText: 2016 - 2019 Gwilherm Baudic
4// SPDX-FileCopyrightText: 2013 - 2026 Fifengine contributors
5
6// Corresponding header include
7#include "fifechan/backends/sdl3/graphics.hpp"
8
9// Standard library includes
10#include <algorithm>
11#include <array>
12#include <cstdio>
13#include <iterator>
14#include <memory>
15#include <numbers>
16#include <string>
17#include <utility>
18#include <vector>
19
20// Third-party library includes
21#include <SDL3/SDL.h>
22
23// Project headers (subdirs before local)
24#include "fifechan/backends/sdl3/image.hpp"
25#include "fifechan/backends/sdl3/truetypefont.hpp"
26#include "fifechan/exception.hpp"
27#include "fifechan/font.hpp"
28#include "fifechan/image.hpp"
29
30namespace fcn::sdl3
31{
32
33 namespace
34 {
35 constexpr float kColorNormalizationFactor = 1.0F / 255.0F;
36
37 void setRenderDrawColor(SDL_Renderer* renderer, fcn::Color const & color)
38 {
39 SDL_SetRenderDrawColorFloat(
40 renderer,
41 static_cast<float>(color.r) * kColorNormalizationFactor,
42 static_cast<float>(color.g) * kColorNormalizationFactor,
43 static_cast<float>(color.b) * kColorNormalizationFactor,
44 static_cast<float>(color.a) * kColorNormalizationFactor);
45 }
46
47 SDL_FColor toSDLVertexColor(fcn::Color const & color)
48 {
49 return SDL_FColor{
50 static_cast<float>(color.r) * kColorNormalizationFactor,
51 static_cast<float>(color.g) * kColorNormalizationFactor,
52 static_cast<float>(color.b) * kColorNormalizationFactor,
53 static_cast<float>(color.a) * kColorNormalizationFactor};
54 }
55
56 SDL_Vertex makeSolidVertex(float x, float y, SDL_FColor const & color)
57 {
58 return SDL_Vertex{.position = {x, y}, .color = color};
59 }
60 } // namespace
61
62 Graphics::Graphics() : mAlpha(false)
63 {
64 }
65
66 Graphics::~Graphics() = default;
67
69 {
70 Rectangle area;
71 area.x = 0;
72 area.y = 0;
73 area.width = mWidth;
74 area.height = mHeight;
75 pushClipArea(area);
76 }
77
79 {
81 }
82
83 void Graphics::setTarget(SDL_Renderer* renderer, int width, int height)
84 {
85 mRenderTarget = renderer;
86 mWidth = width;
87 mHeight = height;
88 }
89
91 {
92 bool const result = fcn::Graphics::pushClipArea(area);
93
94 if (result) {
95 ClipRectangle const & clip_rect = mClipStack.top();
96
97 SDL_Rect rect;
98 rect.x = clip_rect.x;
99 rect.y = clip_rect.y;
100 rect.w = clip_rect.width;
101 rect.h = clip_rect.height;
102 SDL_SetRenderClipRect(mRenderTarget, &rect);
103 }
104
105 return result;
106 }
107
109 {
111
112 if (mClipStack.empty()) {
113 return;
114 }
115
116 ClipRectangle const & clip_rect = mClipStack.top();
117
118 SDL_Rect rect;
119 rect.x = clip_rect.x;
120 rect.y = clip_rect.y;
121 rect.w = clip_rect.width;
122 rect.h = clip_rect.height;
123
124 SDL_SetRenderClipRect(mRenderTarget, &rect);
125 }
126
127 SDL_Renderer* Graphics::getRenderTarget() const
128 {
129 return mRenderTarget;
130 }
131
132 void Graphics::drawImage(fcn::Image const * image, int srcX, int srcY, int dstX, int dstY, int width, int height)
133 {
134 if (mClipStack.empty()) {
136 "Clip stack is empty, perhaps you"
137 "called a draw function outside of _beginDraw() and _endDraw()?");
138 }
139
140 ClipRectangle const & top = mClipStack.top();
141 SDL_FRect src;
142 SDL_FRect dst;
143 src.x = static_cast<float>(srcX);
144 src.y = static_cast<float>(srcY);
145 src.w = static_cast<float>(width);
146 src.h = static_cast<float>(height);
147 dst.x = static_cast<float>(dstX + top.xOffset);
148 dst.y = static_cast<float>(dstY + top.yOffset);
149 dst.w = static_cast<float>(width);
150 dst.h = static_cast<float>(height);
151
152 auto const * srcImage = dynamic_cast<Image const *>(image);
153
154 if (srcImage == nullptr) {
155 throwException("Trying to draw an image of unknown format, must be an Image.");
156 }
157
158 SDL_Texture* texture = srcImage->getTexture();
159 if (texture != nullptr) {
160 SDL_RenderTexture(mRenderTarget, texture, &src, &dst);
161 }
162 }
163
164 void Graphics::fillRectangle(Rectangle const & rectangle)
165 {
166 if (mClipStack.empty()) {
168 "Clip stack is empty, perhaps you"
169 "called a draw function outside of _beginDraw() and _endDraw()?");
170 }
171
172 ClipRectangle const & top = mClipStack.top();
173
174 Rectangle area = rectangle;
175 area.x += top.xOffset;
176 area.y += top.yOffset;
177
178 if (!area.isIntersecting(top)) {
179 return;
180 }
181
182 SDL_FRect rect;
183 rect.x = static_cast<float>(area.x);
184 rect.y = static_cast<float>(area.y);
185 rect.w = static_cast<float>(area.width);
186 rect.h = static_cast<float>(area.height);
187
189 setRenderDrawColor(mRenderTarget, mColor);
190 SDL_RenderFillRect(mRenderTarget, &rect);
192 }
193
194 void Graphics::drawPoint(int x, int y)
195 {
196 if (mClipStack.empty()) {
198 "Clip stack is empty, perhaps you"
199 "called a draw function outside of _beginDraw() and _endDraw()?");
200 }
201
202 ClipRectangle const & top = mClipStack.top();
203
204 x += top.xOffset;
205 y += top.yOffset;
206
207 if (!top.isContaining(x, y)) {
208 return;
209 }
210
212 setRenderDrawColor(mRenderTarget, mColor);
213 SDL_RenderPoint(mRenderTarget, x, y);
215 }
216
217 void Graphics::drawHorizontalLine(int x1, int y, int x2)
218 {
219 if (mClipStack.empty()) {
221 "Clip stack is empty, perhaps you"
222 "called a draw function outside of _beginDraw() and _endDraw()?");
223 }
224 ClipRectangle const & top = mClipStack.top();
225
226 x1 += top.xOffset;
227 y += top.yOffset;
228 x2 += top.xOffset;
229
230 if (y < top.y || y >= top.y + top.height) {
231 return;
232 }
233
234 if (x1 > x2) {
235 x1 ^= x2;
236 x2 ^= x1;
237 x1 ^= x2;
238 }
239
240 if (top.x > x1) {
241 if (top.x > x2) {
242 return;
243 }
244 x1 = top.x;
245 }
246
247 if (top.x + top.width <= x2) {
248 if (top.x + top.width <= x1) {
249 return;
250 }
251 x2 = top.x + top.width - 1;
252 }
253
255 setRenderDrawColor(mRenderTarget, mColor);
256 SDL_RenderLine(mRenderTarget, x1, y, x2, y);
258 }
259
260 void Graphics::drawVerticalLine(int x, int y1, int y2)
261 {
262 if (mClipStack.empty()) {
264 "Clip stack is empty, perhaps you"
265 "called a draw function outside of _beginDraw() and _endDraw()?");
266 }
267 ClipRectangle const & top = mClipStack.top();
268
269 x += top.xOffset;
270 y1 += top.yOffset;
271 y2 += top.yOffset;
272
273 if (x < top.x || x >= top.x + top.width) {
274 return;
275 }
276
277 if (y1 > y2) {
278 y1 ^= y2;
279 y2 ^= y1;
280 y1 ^= y2;
281 }
282
283 if (top.y > y1) {
284 if (top.y > y2) {
285 return;
286 }
287 y1 = top.y;
288 }
289
290 if (top.y + top.height <= y2) {
291 if (top.y + top.height <= y1) {
292 return;
293 }
294 y2 = top.y + top.height - 1;
295 }
296
298 setRenderDrawColor(mRenderTarget, mColor);
299 SDL_RenderLine(mRenderTarget, x, y1, x, y2);
301 }
302
303 void Graphics::drawRectangle(Rectangle const & rectangle)
304 {
305 int const x1 = rectangle.x;
306 int const x2 = rectangle.x + rectangle.width - 1;
307 int const y1 = rectangle.y;
308 int const y2 = rectangle.y + rectangle.height - 1;
309
310 drawHorizontalLine(x1, y1, x2);
311 drawHorizontalLine(x1, y2, x2);
312
313 drawVerticalLine(x1, y1, y2);
314 drawVerticalLine(x2, y1, y2);
315 }
316
317 void Graphics::drawLine(int x1, int y1, int x2, int y2)
318 {
319
320 if (mClipStack.empty()) {
322 "Clip stack is empty, perhaps you"
323 "called a draw function outside of _beginDraw() and _endDraw()?");
324 }
325 ClipRectangle const & top = mClipStack.top();
326
327 x1 += top.xOffset;
328 y1 += top.yOffset;
329 x2 += top.xOffset;
330 y2 += top.yOffset;
331
333 setRenderDrawColor(mRenderTarget, mColor);
334 SDL_RenderLine(mRenderTarget, x1, y1, x2, y2);
336 }
337
338 void Graphics::drawLine(int x1, int y1, int x2, int y2, unsigned int width)
339 {
340 if (mClipStack.empty()) {
342 "Clip stack is empty, perhaps you"
343 "called a draw function outside of _beginDraw() and _endDraw()?");
344 }
345 ClipRectangle const & top = mClipStack.top();
346
347 x1 += top.xOffset;
348 y1 += top.yOffset;
349 x2 += top.xOffset;
350 y2 += top.yOffset;
351
352 if (width <= 1) {
353 drawLine(x1 - top.xOffset, y1 - top.yOffset, x2 - top.xOffset, y2 - top.yOffset);
354 return;
355 }
356
357 if (x1 == x2 && y1 == y2) {
358 // For a single point with width, draw a filled circle
359 drawFillCircle(fcn::Point{x1 - top.xOffset, y1 - top.yOffset}, width / 2);
360 return;
361 }
362
363 // Use SDL_RenderGeometry with quad primitives for thick line
365 setRenderDrawColor(mRenderTarget, mColor);
366
367 auto const dx = static_cast<float>(x2 - x1);
368 auto const dy = static_cast<float>(y2 - y1);
369 float const length = std::sqrt((dx * dx) + (dy * dy));
370
371 if (length < 0.001F) {
373 return;
374 }
375
376 // Calculate perpendicular offset for line width
377 float const offsetX = (dy / length) * (static_cast<float>(width) / 2.0F);
378 float const offsetY = (dx / length) * (static_cast<float>(width) / 2.0F);
379
380 // Create quad vertices (two triangles)
381 std::vector<SDL_Vertex> vertices = {
382 SDL_Vertex{
383 .position = {static_cast<float>(x1) - offsetX, static_cast<float>(y1) + offsetY},
384 .color =
385 {static_cast<float>(mColor.r) / 255.0F,
386 static_cast<float>(mColor.g) / 255.0F,
387 static_cast<float>(mColor.b) / 255.0F,
388 static_cast<float>(mColor.a) / 255.0F}},
389 SDL_Vertex{
390 .position = {static_cast<float>(x1) + offsetX, static_cast<float>(y1) - offsetY},
391 .color =
392 {static_cast<float>(mColor.r) / 255.0F,
393 static_cast<float>(mColor.g) / 255.0F,
394 static_cast<float>(mColor.b) / 255.0F,
395 static_cast<float>(mColor.a) / 255.0F}},
396 SDL_Vertex{
397 .position = {static_cast<float>(x2) + offsetX, static_cast<float>(y2) - offsetY},
398 .color =
399 {static_cast<float>(mColor.r) / 255.0F,
400 static_cast<float>(mColor.g) / 255.0F,
401 static_cast<float>(mColor.b) / 255.0F,
402 static_cast<float>(mColor.a) / 255.0F}},
403 SDL_Vertex{
404 .position = {static_cast<float>(x2) - offsetX, static_cast<float>(y2) + offsetY},
405 .color = {
406 static_cast<float>(mColor.r) / 255.0F,
407 static_cast<float>(mColor.g) / 255.0F,
408 static_cast<float>(mColor.b) / 255.0F,
409 static_cast<float>(mColor.a) / 255.0F}}};
410
411 // Two triangles forming a quad
412 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
413
414 SDL_RenderGeometry(
416 nullptr,
417 vertices.data(),
418 static_cast<int>(vertices.size()),
419 indices.data(),
420 static_cast<int>(indices.size()));
421
423 }
424
425 void Graphics::drawRoundStroke(int x1, int y1, int x2, int y2, unsigned int width)
426 {
427 if (mClipStack.empty()) {
429 "Clip stack is empty, perhaps you"
430 "called a draw function outside of _beginDraw() and _endDraw()?");
431 }
432 ClipRectangle const & top = mClipStack.top();
433
434 x1 += top.xOffset;
435 y1 += top.yOffset;
436 x2 += top.xOffset;
437 y2 += top.yOffset;
438
439 if (width <= 1) {
440 drawLine(x1 - top.xOffset, y1 - top.yOffset, x2 - top.xOffset, y2 - top.yOffset);
441 return;
442 }
443
444 int const radius = std::max(1, static_cast<int>(width) / 2);
445
446 // For a single point, just draw a filled circle
447 if (x1 == x2 && y1 == y2) {
448 drawFillCircle(fcn::Point{x1 - top.xOffset, y1 - top.yOffset}, static_cast<unsigned int>(radius));
449 return;
450 }
451
452 // Draw filled circles along the line path using SDL_RenderGeometry
454 setRenderDrawColor(mRenderTarget, mColor);
455
456 int const dx = x2 - x1;
457 int const dy = y2 - y1;
458 int const stepCount = std::max(std::abs(dx), std::abs(dy));
459
460 // For each step, draw a filled circle using triangle fan
461 // Use adaptive segment count based on radius for quality
462 int const circleSegments = std::max(8, radius * 2);
463
464 for (int step = 0; step <= stepCount; ++step) {
465 int const centerX = x1 + ((dx * step) / stepCount);
466 int const centerY = y1 + ((dy * step) / stepCount);
467
468 // Generate triangle fan vertices for filled circle
469 std::vector<SDL_Vertex> vertices;
470 vertices.reserve(circleSegments + 2);
471
472 // Center vertex
473 vertices.push_back(
474 SDL_Vertex{
475 .position = {static_cast<float>(centerX), static_cast<float>(centerY)},
476 .color = {
477 static_cast<float>(mColor.r) / 255.0F,
478 static_cast<float>(mColor.g) / 255.0F,
479 static_cast<float>(mColor.b) / 255.0F,
480 static_cast<float>(mColor.a) / 255.0F}});
481
482 // Circle edge vertices
483 for (int i = 0; i <= circleSegments; ++i) {
484 float const angle =
485 2.0f * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(circleSegments);
486 float const x = static_cast<float>(centerX) + (radius * std::cos(angle));
487 float const y = static_cast<float>(centerY) + (radius * std::sin(angle));
488 vertices.push_back(
489 SDL_Vertex{
490 .position = {x, y},
491 .color = {
492 static_cast<float>(mColor.r) / 255.0F,
493 static_cast<float>(mColor.g) / 255.0F,
494 static_cast<float>(mColor.b) / 255.0F,
495 static_cast<float>(mColor.a) / 255.0F}});
496 }
497
498 // Generate indices for triangle fan
499 std::vector<int> indices;
500 indices.reserve(circleSegments * 3);
501 for (int i = 1; i <= circleSegments; ++i) {
502 indices.push_back(0); // Center
503 indices.push_back(i);
504 indices.push_back(i + 1 <= circleSegments ? i + 1 : 1);
505 }
506
507 SDL_RenderGeometry(
509 nullptr,
510 vertices.data(),
511 static_cast<int>(vertices.size()),
512 indices.data(),
513 static_cast<int>(indices.size()));
514 }
515
517 }
518
519 void Graphics::drawFillCircle(fcn::Point const & center, unsigned int radius)
520 {
521 if (mClipStack.empty()) {
523 "Clip stack is empty, perhaps you"
524 "called a draw function outside of _beginDraw() and _endDraw()?");
525 }
526 ClipRectangle const & top = mClipStack.top();
527
528 int const x0 = center.x + top.xOffset;
529 int const y0 = center.y + top.yOffset;
530
531 // Use SDL_RenderGeometry for hardware-accelerated circle rendering
532 // Generate triangle fan vertices for the circle
533 int const numSegments = std::max(8, static_cast<int>(radius / 2));
534 std::vector<SDL_Vertex> vertices;
535 vertices.reserve(numSegments + 2);
536 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
537
538 // Center vertex
539 vertices.push_back(makeSolidVertex(static_cast<float>(x0), static_cast<float>(y0), vertexColor));
540
541 // Circle edge vertices
542 for (int i = 0; i <= numSegments; ++i) {
543 float const angle =
544 2.0f * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(numSegments);
545 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
546 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
547 vertices.push_back(makeSolidVertex(x, y, vertexColor));
548 }
549
550 // Generate indices for triangle fan
551 std::vector<int> indices;
552 indices.reserve(numSegments * 3);
553 for (int i = 1; i <= numSegments; ++i) {
554 indices.push_back(0); // Center
555 indices.push_back(i);
556 indices.push_back(i + 1 <= numSegments ? i + 1 : 1);
557 }
558
560 setRenderDrawColor(mRenderTarget, mColor);
561 SDL_RenderGeometry(
563 nullptr,
564 vertices.data(),
565 static_cast<int>(vertices.size()),
566 indices.data(),
567 static_cast<int>(indices.size()));
569 }
570
571 namespace
572 {
573 fcn::Point bezierPoint(std::vector<fcn::Point> const & controlPoints, float t)
574 {
575 std::vector<fcn::Point> points = controlPoints;
576 while (points.size() > 1) {
577 std::vector<fcn::Point> nextPoints;
578 for (size_t i = 0; i < points.size() - 1; ++i) {
579 int const x = static_cast<int>(((1 - t) * points[i].x) + (t * points[i + 1].x));
580 int const y = static_cast<int>(((1 - t) * points[i].y) + (t * points[i + 1].y));
581 nextPoints.emplace_back(x, y);
582 }
583 points = nextPoints;
584 }
585 return points[0];
586 }
587
588 constexpr float degToRad(float degrees)
589 {
590 return degrees * std::numbers::pi_v<float> / 180.0F;
591 }
592 } // namespace
593
594 void Graphics::drawFillCircleSegment(fcn::Point const & center, unsigned int radius, int startAngle, int endAngle)
595 {
596 if (mClipStack.empty()) {
598 "Clip stack is empty, perhaps you"
599 "called a draw function outside of _beginDraw() and _endDraw()?");
600 }
601 ClipRectangle const & top = mClipStack.top();
602
603 int const x0 = center.x + top.xOffset;
604 int const y0 = center.y + top.yOffset;
605
606 // Normalize angles
607 startAngle = startAngle % 360;
608 endAngle = endAngle % 360;
609
610 if (endAngle < startAngle) {
611 endAngle += 360;
612 }
613
614 // Use SDL_RenderGeometry for hardware-accelerated filled circle segment
615 // Generate triangle fan vertices for the circle segment
616 int const numSegments = std::max(8, static_cast<int>(radius / 2));
617 std::vector<SDL_Vertex> vertices;
618 vertices.reserve(numSegments + 2);
619 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
620
621 // Center vertex
622 vertices.push_back(makeSolidVertex(static_cast<float>(x0), static_cast<float>(y0), vertexColor));
623
624 float const startRad = static_cast<float>(startAngle) * std::numbers::pi_v<float> / 180.0F;
625 float const endRad = static_cast<float>(endAngle) * std::numbers::pi_v<float> / 180.0F;
626
627 for (int i = 0; i <= numSegments; ++i) {
628 float const t = static_cast<float>(i) / static_cast<float>(numSegments);
629 float const angle = startRad + (t * (endRad - startRad));
630 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
631 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
632 vertices.push_back(makeSolidVertex(x, y, vertexColor));
633 }
634
635 // Generate indices for triangle fan
636 std::vector<int> indices;
637 indices.reserve(numSegments * 3);
638 for (int i = 1; i <= numSegments; ++i) {
639 indices.push_back(0); // Center
640 indices.push_back(i);
641 indices.push_back(i + 1);
642 }
643
645 setRenderDrawColor(mRenderTarget, mColor);
646 SDL_RenderGeometry(
648 nullptr,
649 vertices.data(),
650 static_cast<int>(vertices.size()),
651 indices.data(),
652 static_cast<int>(indices.size()));
654 }
655
656 namespace
657 {
658 int normalizeAngle(int angle)
659 {
660 angle %= 360;
661 if (angle < 0) {
662 angle += 360;
663 }
664 return angle;
665 }
666 } // namespace
667
668 void Graphics::drawCircle(fcn::Point const & center, unsigned int radius)
669 {
670 if (mClipStack.empty()) {
672 "Clip stack is empty, perhaps you"
673 "called a draw function outside of _beginDraw() and _endDraw()?");
674 }
675 ClipRectangle const & top = mClipStack.top();
676
677 int const x0 = center.x + top.xOffset;
678 int const y0 = center.y + top.yOffset;
679
680 // Use SDL_RenderGeometry for hardware-accelerated circle outline
681 // Generate vertices along the circle and connect with line segments
682 int const numSegments = std::max(8, static_cast<int>(radius / 2));
683 std::vector<SDL_Vertex> vertices;
684 vertices.reserve(numSegments + 1);
685 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
686
687 for (int i = 0; i <= numSegments; ++i) {
688 float const angle =
689 2.0f * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(numSegments);
690 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
691 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
692 vertices.push_back(makeSolidVertex(x, y, vertexColor));
693 }
694
695 // Generate indices for line loop (connect consecutive vertices)
696 std::vector<int> indices;
697 indices.reserve(numSegments * 2);
698 for (int i = 0; i < numSegments; ++i) {
699 indices.push_back(i);
700 indices.push_back(i + 1);
701 }
702
704 setRenderDrawColor(mRenderTarget, mColor);
705 SDL_RenderGeometry(
707 nullptr,
708 vertices.data(),
709 static_cast<int>(vertices.size()),
710 indices.data(),
711 static_cast<int>(indices.size()));
713 }
714
715 void Graphics::drawCircleSegment(fcn::Point const & center, unsigned int radius, int startAngle, int endAngle)
716 {
717 if (mClipStack.empty()) {
719 "Clip stack is empty, perhaps you"
720 "called a draw function outside of _beginDraw() and _endDraw()?");
721 }
722 ClipRectangle const & top = mClipStack.top();
723
724 int const x0 = center.x + top.xOffset;
725 int const y0 = center.y + top.yOffset;
726
727 // Normalize angles
728 startAngle = normalizeAngle(startAngle);
729 endAngle = normalizeAngle(endAngle);
730
731 if (endAngle < startAngle) {
732 endAngle += 360;
733 }
734
735 // Use SDL_RenderGeometry for hardware-accelerated circle segment outline
736 // Generate vertices along the arc from startAngle to endAngle
737 int const numSegments = std::max(8, static_cast<int>(radius / 2));
738 std::vector<SDL_Vertex> vertices;
739 vertices.reserve(numSegments + 1);
740 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
741
742 float const startRad = static_cast<float>(startAngle) * std::numbers::pi_v<float> / 180.0F;
743 float const endRad = static_cast<float>(endAngle) * std::numbers::pi_v<float> / 180.0F;
744
745 for (int i = 0; i <= numSegments; ++i) {
746 float const t = static_cast<float>(i) / static_cast<float>(numSegments);
747 float const angle = startRad + (t * (endRad - startRad));
748 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
749 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
750 vertices.push_back(makeSolidVertex(x, y, vertexColor));
751 }
752
753 // Generate indices for line loop
754 std::vector<int> indices;
755 indices.reserve(numSegments * 2);
756 for (int i = 0; i < numSegments; ++i) {
757 indices.push_back(i);
758 indices.push_back(i + 1);
759 }
760
762 setRenderDrawColor(mRenderTarget, mColor);
763 SDL_RenderGeometry(
765 nullptr,
766 vertices.data(),
767 static_cast<int>(vertices.size()),
768 indices.data(),
769 static_cast<int>(indices.size()));
771 }
772
773 void Graphics::drawBezier(PointVector const & controlPoints, int segments, unsigned int width)
774 {
775 if (mClipStack.empty()) {
777 "Clip stack is empty, perhaps you"
778 "called a draw function outside of _beginDraw() and _endDraw()?");
779 }
780 ClipRectangle const & top = mClipStack.top();
781
782 if (segments < 1) {
783 return;
784 }
785
786 // Generate all points along the Bezier curve
787 std::vector<SDL_FPoint> points;
788 points.reserve(segments + 1);
789
790 for (int i = 0; i <= segments; ++i) {
791 float const t = static_cast<float>(i) / static_cast<float>(segments);
792 fcn::Point const point = bezierPoint(controlPoints, t);
793 points.push_back(
794 SDL_FPoint{static_cast<float>(point.x + top.xOffset), static_cast<float>(point.y + top.yOffset)});
795 }
796
797 if (width <= 1) {
798 // Use SDL_RenderLines for thin lines (batched rendering)
800 setRenderDrawColor(mRenderTarget, mColor);
801 SDL_RenderLines(mRenderTarget, points.data(), static_cast<int>(points.size()));
803 } else {
804 // For thick lines, draw multiple quads along the path using SDL_RenderGeometry
806 setRenderDrawColor(mRenderTarget, mColor);
807
808 float const halfWidth = static_cast<float>(width) / 2.0F;
809
810 for (size_t i = 0; i < points.size() - 1; ++i) {
811 float const x1 = points[i].x;
812 float const y1 = points[i].y;
813 float const x2 = points[i + 1].x;
814 float const y2 = points[i + 1].y;
815
816 float const dx = x2 - x1;
817 float const dy = y2 - y1;
818 float const length = std::sqrt((dx * dx) + (dy * dy));
819
820 if (length < 0.001F) {
821 continue;
822 }
823
824 // Calculate perpendicular offset for line width
825 float const offsetX = (dy / length) * halfWidth;
826 float const offsetY = (dx / length) * halfWidth;
827
828 // Create quad vertices (two triangles)
829 std::vector<SDL_Vertex> vertices = {
830 SDL_Vertex{
831 .position = {x1 - offsetX, y1 + offsetY},
832 .color =
833 {static_cast<float>(mColor.r) / 255.0F,
834 static_cast<float>(mColor.g) / 255.0F,
835 static_cast<float>(mColor.b) / 255.0F,
836 static_cast<float>(mColor.a) / 255.0F}},
837 SDL_Vertex{
838 .position = {x1 + offsetX, y1 - offsetY},
839 .color =
840 {static_cast<float>(mColor.r) / 255.0F,
841 static_cast<float>(mColor.g) / 255.0F,
842 static_cast<float>(mColor.b) / 255.0F,
843 static_cast<float>(mColor.a) / 255.0F}},
844 SDL_Vertex{
845 .position = {x2 + offsetX, y2 - offsetY},
846 .color =
847 {static_cast<float>(mColor.r) / 255.0F,
848 static_cast<float>(mColor.g) / 255.0F,
849 static_cast<float>(mColor.b) / 255.0F,
850 static_cast<float>(mColor.a) / 255.0F}},
851 SDL_Vertex{
852 .position = {x2 - offsetX, y2 + offsetY},
853 .color = {
854 static_cast<float>(mColor.r) / 255.0F,
855 static_cast<float>(mColor.g) / 255.0F,
856 static_cast<float>(mColor.b) / 255.0F,
857 static_cast<float>(mColor.a) / 255.0F}}};
858
859 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
860 SDL_RenderGeometry(
862 nullptr,
863 vertices.data(),
864 static_cast<int>(vertices.size()),
865 indices.data(),
866 static_cast<int>(indices.size()));
867 }
868
870 }
871 }
872
873 void Graphics::drawPolyLine(PointVector const & points, unsigned int width)
874 {
875 if (mClipStack.empty()) {
877 "Clip stack is empty, perhaps you"
878 "called a draw function outside of _beginDraw() and _endDraw()?");
879 }
880
881 if (points.size() < 2) {
882 return;
883 }
884
885 ClipRectangle const & top = mClipStack.top();
886
887 // Convert points to SDL_FPoint array with offset applied
888 std::vector<SDL_FPoint> sdlPoints;
889 sdlPoints.reserve(points.size());
890 std::transform(points.begin(), points.end(), std::back_inserter(sdlPoints), [&top](auto const & point) {
891 return SDL_FPoint{static_cast<float>(point.x + top.xOffset), static_cast<float>(point.y + top.yOffset)};
892 });
893
894 if (width <= 1) {
895 // Use SDL_RenderLines for batched rendering of thin lines
896 saveRenderColor();
897 setRenderDrawColor(mRenderTarget, mColor);
898 SDL_RenderLines(mRenderTarget, sdlPoints.data(), static_cast<int>(sdlPoints.size()));
899 restoreRenderColor();
900 } else {
901 // For thick lines, draw quads between each pair of points
902 saveRenderColor();
903 setRenderDrawColor(mRenderTarget, mColor);
904
905 float const halfWidth = static_cast<float>(width) / 2.0F;
906
907 for (size_t i = 0; i < sdlPoints.size() - 1; ++i) {
908 float const x1 = sdlPoints[i].x;
909 float const y1 = sdlPoints[i].y;
910 float const x2 = sdlPoints[i + 1].x;
911 float const y2 = sdlPoints[i + 1].y;
912
913 float const dx = x2 - x1;
914 float const dy = y2 - y1;
915 float const length = std::sqrt((dx * dx) + (dy * dy));
916
917 if (length < 0.001F) {
918 continue;
919 }
920
921 // Calculate perpendicular offset for line width
922 float const offsetX = (dy / length) * halfWidth;
923 float const offsetY = (dx / length) * halfWidth;
924
925 // Create quad vertices (two triangles)
926 std::vector<SDL_Vertex> vertices = {
927 SDL_Vertex{
928 .position = {x1 - offsetX, y1 + offsetY},
929 .color =
930 {static_cast<float>(mColor.r) / 255.0F,
931 static_cast<float>(mColor.g) / 255.0F,
932 static_cast<float>(mColor.b) / 255.0F,
933 static_cast<float>(mColor.a) / 255.0F}},
934 SDL_Vertex{
935 .position = {x1 + offsetX, y1 - offsetY},
936 .color =
937 {static_cast<float>(mColor.r) / 255.0F,
938 static_cast<float>(mColor.g) / 255.0F,
939 static_cast<float>(mColor.b) / 255.0F,
940 static_cast<float>(mColor.a) / 255.0F}},
941 SDL_Vertex{
942 .position = {x2 + offsetX, y2 - offsetY},
943 .color =
944 {static_cast<float>(mColor.r) / 255.0F,
945 static_cast<float>(mColor.g) / 255.0F,
946 static_cast<float>(mColor.b) / 255.0F,
947 static_cast<float>(mColor.a) / 255.0F}},
948 SDL_Vertex{
949 .position = {x2 - offsetX, y2 + offsetY},
950 .color = {
951 static_cast<float>(mColor.r) / 255.0F,
952 static_cast<float>(mColor.g) / 255.0F,
953 static_cast<float>(mColor.b) / 255.0F,
954 static_cast<float>(mColor.a) / 255.0F}}};
955
956 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
957 SDL_RenderGeometry(
958 mRenderTarget,
959 nullptr,
960 vertices.data(),
961 static_cast<int>(vertices.size()),
962 indices.data(),
963 static_cast<int>(indices.size()));
964 }
965
966 restoreRenderColor();
967 }
968 }
969
970 void Graphics::setColor(Color const & color)
971 {
972 mColor = color;
973
974 mAlpha = color.a != 255;
975 }
976
977 Color const & Graphics::getColor() const
978 {
979 return mColor;
980 }
981
982 std::shared_ptr<Font> Graphics::createFont(std::string const & filename, int size)
983 {
984 return std::make_shared<TrueTypeFont>(filename, size);
985 }
986
987 void Graphics::drawSDLTexture(SDL_Texture* texture, SDL_FRect source, SDL_FRect destination)
988 {
989 if (mClipStack.empty()) {
991 "Clip stack is empty, perhaps you"
992 "called a draw function outside of _beginDraw() and _endDraw()?");
993 }
994
995 ClipRectangle const & top = mClipStack.top();
996
997 destination.x += static_cast<float>(top.xOffset);
998 destination.y += static_cast<float>(top.yOffset);
999 destination.w = source.w;
1000 destination.h = source.h;
1001
1002 SDL_RenderTexture(mRenderTarget, texture, &source, &destination);
1003 }
1004
1006 {
1007 SDL_GetRenderDrawColor(mRenderTarget, &r, &g, &b, &a);
1008 }
1009
1011 {
1012 SDL_SetRenderDrawColor(mRenderTarget, r, g, b, a);
1013 }
1014
1015} // namespace fcn::sdl3
A rectangle specifically used for clipping rendering regions.
int xOffset
Holds the x offset of the x coordinate.
int yOffset
Holds the y offset of the y coordinate.
Color.
Definition color.hpp:58
uint8_t a
Alpha color component (0-255).
Definition color.hpp:350
uint8_t b
Blue color component (0-255).
Definition color.hpp:347
uint8_t g
Green color component (0-255).
Definition color.hpp:344
uint8_t r
Red color component (0-255).
Definition color.hpp:341
virtual void popClipArea()
Removes the top most clip area from the stack.
Definition graphics.cpp:65
virtual bool pushClipArea(Rectangle area)
Pushes a clip area onto the stack.
Definition graphics.cpp:25
std::stack< ClipRectangle > mClipStack
Holds the clip area stack.
Definition graphics.hpp:421
Abstract holder for image data.
Definition image.hpp:34
Represents a 2D coordinate (X, Y).
Definition point.hpp:34
Represents a rectangular area (X, Y, Width, Height).
Definition rectangle.hpp:22
bool isContaining(int x, int y) const
Checks the rectangle contains a point.
Definition rectangle.cpp:63
int width
Holds the width of the rectangle.
int y
Holds the x coordinate of the rectangle.
int x
Holds the x coordinate of the rectangle.
int height
Holds the height of the rectangle.
bool isIntersecting(Rectangle const &rectangle) const
Checks if another rectangle intersects with the rectangle.
Definition rectangle.cpp:34
Uint8 a
Previous alpha component from renderer.
void restoreRenderColor()
Restore the rendering color after drawing.
void drawFillCircle(Point const &center, unsigned int radius) override
Draws a filled circle.
void drawPoint(int x, int y) override
Draws a single point/pixel.
void drawImage(fcn::Image const *image, int srcX, int srcY, int dstX, int dstY, int width, int height) override
Draws a part of an image.
void drawRoundStroke(int x1, int y1, int x2, int y2, unsigned int width) override
Draws a round brush stroke along the line segment.
void saveRenderColor()
Save the current rendering color before drawing.
virtual SDL_Renderer * getRenderTarget() const
Gets the target SDL_Renderer.
void drawFillCircleSegment(Point const &center, unsigned int radius, int startAngle, int endAngle) override
Draws a filled circle segment.
Color const & getColor() const override
Gets the color to use when drawing.
void _beginDraw() override
Initializes drawing.
void drawPolyLine(PointVector const &points, unsigned int width) override
Draws lines between points with given width.
SDL_Renderer * mRenderTarget
The SDL_Renderer used for accelerated drawing.
void drawRectangle(Rectangle const &rectangle) override
Draws a simple, non-filled rectangle with a one pixel width.
Uint8 r
Cached renderer color components (previous renderer color).
void _endDraw() override
Deinitializes the drawing process.
void drawLine(int x1, int y1, int x2, int y2) override
Draws a line.
void drawHorizontalLine(int x1, int y, int x2)
Draws a horizontal line.
void drawCircle(Point const &center, unsigned int radius) override
Draws a simple, non-filled circle with a one pixel width.
virtual void drawSDLTexture(SDL_Texture *texture, SDL_FRect source, SDL_FRect destination)
Draws an SDL_Texture on the target surface.
void drawVerticalLine(int x, int y1, int y2)
Draws a vertical line.
void setColor(Color const &color) override
Sets the color to use when drawing.
bool mAlpha
Whether alpha blending is enabled.
bool pushClipArea(fcn::Rectangle area) override
Pushes a clip area onto the stack.
void fillRectangle(Rectangle const &rectangle) override
Draws a filled rectangle.
void popClipArea() override
Removes the top most clip area from the stack.
void drawCircleSegment(Point const &center, unsigned int radius, int startAngle, int endAngle) override
Draws a simple, non-filled circle segment with a one pixel width.
Uint8 b
Previous blue component from renderer.
Uint8 g
Previous green component from renderer.
std::shared_ptr< Font > createFont(std::string const &filename, int size) override
Creates a font for this graphics backend.
void drawBezier(PointVector const &points, int segments, unsigned int width) override
Draws a bezier curve.
Color mColor
Current drawing color.
virtual void setTarget(SDL_Renderer *renderer, int width, int height)
Sets the target SDL_Renderer to use for drawing.
SDL3-specific implementation of Image.
virtual SDL_Texture * getTexture() const
Gets the SDL texture for the image.
std::vector< Point > PointVector
A list of points.
Definition point.hpp:331
void throwException(std::string const &message, std::source_location location=std::source_location::current())
Throw an Exception capturing the current source location.