FifeGUI 0.3.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 .r = static_cast<float>(color.r) * kColorNormalizationFactor,
51 .g = static_cast<float>(color.g) * kColorNormalizationFactor,
52 .b = static_cast<float>(color.b) * kColorNormalizationFactor,
53 .a = 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 = x, .y = 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 // TODO: remove format off. this formatting is inconsistent between clang-format versions
382 // clang-format off
383 std::vector<SDL_Vertex> vertices = {
384 SDL_Vertex{
385 .position = {.x = static_cast<float>(x1) - offsetX, .y = static_cast<float>(y1) + offsetY},
386 .color = {
387 .r = static_cast<float>(mColor.r) / 255.0F,
388 .g = static_cast<float>(mColor.g) / 255.0F,
389 .b = static_cast<float>(mColor.b) / 255.0F,
390 .a = static_cast<float>(mColor.a) / 255.0F}},
391 SDL_Vertex{
392 .position = {.x = static_cast<float>(x1) + offsetX, .y = static_cast<float>(y1) - offsetY},
393 .color = {
394 .r = static_cast<float>(mColor.r) / 255.0F,
395 .g = static_cast<float>(mColor.g) / 255.0F,
396 .b = static_cast<float>(mColor.b) / 255.0F,
397 .a = static_cast<float>(mColor.a) / 255.0F}},
398 SDL_Vertex{
399 .position = {.x = static_cast<float>(x2) + offsetX, .y = static_cast<float>(y2) - offsetY},
400 .color = {
401 .r = static_cast<float>(mColor.r) / 255.0F,
402 .g = static_cast<float>(mColor.g) / 255.0F,
403 .b = static_cast<float>(mColor.b) / 255.0F,
404 .a = static_cast<float>(mColor.a) / 255.0F}},
405 SDL_Vertex{
406 .position = {.x = static_cast<float>(x2) - offsetX, .y = static_cast<float>(y2) + offsetY},
407 .color = {
408 .r = static_cast<float>(mColor.r) / 255.0F,
409 .g = static_cast<float>(mColor.g) / 255.0F,
410 .b = static_cast<float>(mColor.b) / 255.0F,
411 .a = static_cast<float>(mColor.a) / 255.0F}}};
412 // clang-format on
413
414 // Two triangles forming a quad
415 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
416
417 SDL_RenderGeometry(
419 nullptr,
420 vertices.data(),
421 static_cast<int>(vertices.size()),
422 indices.data(),
423 static_cast<int>(indices.size()));
424
426 }
427
428 void Graphics::drawRoundStroke(int x1, int y1, int x2, int y2, unsigned int width)
429 {
430 if (mClipStack.empty()) {
432 "Clip stack is empty, perhaps you"
433 "called a draw function outside of _beginDraw() and _endDraw()?");
434 }
435 ClipRectangle const & top = mClipStack.top();
436
437 x1 += top.xOffset;
438 y1 += top.yOffset;
439 x2 += top.xOffset;
440 y2 += top.yOffset;
441
442 if (width <= 1) {
443 drawLine(x1 - top.xOffset, y1 - top.yOffset, x2 - top.xOffset, y2 - top.yOffset);
444 return;
445 }
446
447 int const radius = std::max(1, static_cast<int>(width) / 2);
448
449 // For a single point, just draw a filled circle
450 if (x1 == x2 && y1 == y2) {
451 drawFillCircle(fcn::Point{x1 - top.xOffset, y1 - top.yOffset}, static_cast<unsigned int>(radius));
452 return;
453 }
454
455 // Draw filled circles along the line path using SDL_RenderGeometry
457 setRenderDrawColor(mRenderTarget, mColor);
458
459 int const dx = x2 - x1;
460 int const dy = y2 - y1;
461 int const stepCount = std::max(std::abs(dx), std::abs(dy));
462
463 // For each step, draw a filled circle using triangle fan
464 // Use adaptive segment count based on radius for quality
465 int const circleSegments = std::max(8, radius * 2);
466
467 for (int step = 0; step <= stepCount; ++step) {
468 int const centerX = x1 + ((dx * step) / stepCount);
469 int const centerY = y1 + ((dy * step) / stepCount);
470
471 // Generate triangle fan vertices for filled circle
472 std::vector<SDL_Vertex> vertices;
473 vertices.reserve(circleSegments + 2);
474
475 // Center vertex
476 // TODO: remove format off. this formatting is inconsistent between clang-format versions
477 // clang-format off
478 vertices.push_back(
479 SDL_Vertex{
480 .position = {.x = static_cast<float>(centerX), .y = static_cast<float>(centerY)},
481 .color = {
482 .r = static_cast<float>(mColor.r) / 255.0F,
483 .g = static_cast<float>(mColor.g) / 255.0F,
484 .b = static_cast<float>(mColor.b) / 255.0F,
485 .a = static_cast<float>(mColor.a) / 255.0F}});
486 // clang-format on
487
488 // Circle edge vertices
489 for (int i = 0; i <= circleSegments; ++i) {
490 float const angle =
491 2.0F * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(circleSegments);
492 float const x = static_cast<float>(centerX) + (radius * std::cos(angle));
493 float const y = static_cast<float>(centerY) + (radius * std::sin(angle));
494 // TODO: remove format off. this formatting is inconsistent between clang-format versions
495 // clang-format off
496 vertices.push_back(
497 SDL_Vertex{
498 .position = {.x = x, .y = y},
499 .color = {
500 .r = static_cast<float>(mColor.r) / 255.0F,
501 .g = static_cast<float>(mColor.g) / 255.0F,
502 .b = static_cast<float>(mColor.b) / 255.0F,
503 .a = static_cast<float>(mColor.a) / 255.0F}});
504 // clang-format on
505 }
506
507 // Generate indices for triangle fan
508 std::vector<int> indices;
509 indices.reserve(static_cast<std::size_t>(circleSegments) * 3);
510 for (int i = 1; i <= circleSegments; ++i) {
511 indices.push_back(0); // Center
512 indices.push_back(i);
513 indices.push_back(i + 1 <= circleSegments ? i + 1 : 1);
514 }
515
516 SDL_RenderGeometry(
518 nullptr,
519 vertices.data(),
520 static_cast<int>(vertices.size()),
521 indices.data(),
522 static_cast<int>(indices.size()));
523 }
524
526 }
527
528 void Graphics::drawFillCircle(fcn::Point const & center, unsigned int radius)
529 {
530 if (mClipStack.empty()) {
532 "Clip stack is empty, perhaps you"
533 "called a draw function outside of _beginDraw() and _endDraw()?");
534 }
535 ClipRectangle const & top = mClipStack.top();
536
537 int const x0 = center.x + top.xOffset;
538 int const y0 = center.y + top.yOffset;
539
540 // Use SDL_RenderGeometry for hardware-accelerated circle rendering
541 // Generate triangle fan vertices for the circle
542 int const numSegments = std::max(8, static_cast<int>(radius / 2));
543 std::vector<SDL_Vertex> vertices;
544 vertices.reserve(numSegments + 2);
545 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
546
547 // Center vertex
548 vertices.push_back(makeSolidVertex(static_cast<float>(x0), static_cast<float>(y0), vertexColor));
549
550 // Circle edge vertices
551 for (int i = 0; i <= numSegments; ++i) {
552 float const angle =
553 2.0F * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(numSegments);
554 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
555 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
556 vertices.push_back(makeSolidVertex(x, y, vertexColor));
557 }
558
559 // Generate indices for triangle fan
560 std::vector<int> indices;
561 indices.reserve(static_cast<std::size_t>(numSegments) * 3);
562 for (int i = 1; i <= numSegments; ++i) {
563 indices.push_back(0); // Center
564 indices.push_back(i);
565 indices.push_back(i + 1);
566 }
567
569 setRenderDrawColor(mRenderTarget, mColor);
570 SDL_RenderGeometry(
572 nullptr,
573 vertices.data(),
574 static_cast<int>(vertices.size()),
575 indices.data(),
576 static_cast<int>(indices.size()));
578 }
579
580 namespace
581 {
582 int normalizeAngle(int angle)
583 {
584 angle %= 360;
585 if (angle < 0) {
586 angle += 360;
587 }
588 return angle;
589 }
590
591 fcn::Point bezierPoint(fcn::PointVector const & controlPoints, float t)
592 {
593 // De Casteljau's algorithm for Bezier curves
594 std::vector<fcn::Point> points = controlPoints;
595 while (points.size() > 1) {
596 std::vector<fcn::Point> next;
597 for (size_t i = 0; i + 1 < points.size(); ++i) {
598 float const x = ((1.0F - t) * static_cast<float>(points.at(i).x)) +
599 (t * static_cast<float>(points.at(i + 1).x));
600 float const y = ((1.0F - t) * static_cast<float>(points.at(i).y)) +
601 (t * static_cast<float>(points.at(i + 1).y));
602 next.emplace_back(static_cast<int>(x), static_cast<int>(y));
603 }
604 points = std::move(next);
605 }
606 return points.empty() ? fcn::Point{} : points.at(0);
607 }
608 } // namespace
609
610 void Graphics::drawCircle(fcn::Point const & center, unsigned int radius)
611 {
612 if (mClipStack.empty()) {
614 "Clip stack is empty, perhaps you"
615 "called a draw function outside of _beginDraw() and _endDraw()?");
616 }
617 ClipRectangle const & top = mClipStack.top();
618
619 int const x0 = center.x + top.xOffset;
620 int const y0 = center.y + top.yOffset;
621
622 // Use SDL_RenderGeometry for hardware-accelerated circle outline
623 // Generate vertices along the circle and connect with line segments
624 int const numSegments = std::max(8, static_cast<int>(radius / 2));
625 std::vector<SDL_Vertex> vertices;
626 vertices.reserve(numSegments + 1);
627 SDL_FColor const vertexColor = toSDLVertexColor(mColor);
628
629 for (int i = 0; i <= numSegments; ++i) {
630 float const angle =
631 2.0F * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(numSegments);
632 float const x = static_cast<float>(x0) + (radius * std::cos(angle));
633 float const y = static_cast<float>(y0) + (radius * std::sin(angle));
634 vertices.push_back(makeSolidVertex(x, y, vertexColor));
635 }
636
637 // Generate indices for line loop
638 std::vector<int> indices;
639 indices.reserve(static_cast<std::size_t>(numSegments) * 2);
640 for (int i = 0; i < numSegments; ++i) {
641 indices.push_back(i);
642 indices.push_back(i + 1);
643 }
644
646 setRenderDrawColor(mRenderTarget, mColor);
647 SDL_RenderGeometry(
649 nullptr,
650 vertices.data(),
651 static_cast<int>(vertices.size()),
652 indices.data(),
653 static_cast<int>(indices.size()));
655 }
656
658 Point const & /*center*/, unsigned int /*radius*/, int /*startAngle*/, int /*endAngle*/)
659 {
660 // TODO: Implement this function
661 }
662
664 Point const & /*center*/, unsigned int /*radius*/, int /*startAngle*/, int /*endAngle*/)
665 {
666 // TODO: Implement this function
667 }
668
669 void Graphics::drawBezier(PointVector const & controlPoints, int segments, unsigned int width)
670 {
671 if (mClipStack.empty()) {
673 "Clip stack is empty, perhaps you"
674 "called a draw function outside of _beginDraw() and _endDraw()?");
675 }
676 ClipRectangle const & top = mClipStack.top();
677
678 if (segments < 1) {
679 return;
680 }
681
682 // Generate all points along the Bezier curve
683 std::vector<SDL_FPoint> points;
684 points.reserve(segments + 1);
685
686 for (int i = 0; i <= segments; ++i) {
687 float const t = static_cast<float>(i) / static_cast<float>(segments);
688 fcn::Point const point = bezierPoint(controlPoints, t);
689 points.push_back(
690 SDL_FPoint{
691 .x = static_cast<float>(point.x + top.xOffset), .y = static_cast<float>(point.y + top.yOffset)});
692 }
693
694 if (width <= 1) {
695 // Use SDL_RenderLines for thin lines (batched rendering)
697 setRenderDrawColor(mRenderTarget, mColor);
698 SDL_RenderLines(mRenderTarget, points.data(), static_cast<int>(points.size()));
700 } else {
701 // For thick lines, draw multiple quads along the path using SDL_RenderGeometry
703 setRenderDrawColor(mRenderTarget, mColor);
704
705 float const halfWidth = static_cast<float>(width) / 2.0F;
706
707 for (size_t i = 0; i < points.size() - 1; ++i) {
708 float const x1 = points.at(i).x;
709 float const y1 = points.at(i).y;
710 float const x2 = points.at(i + 1).x;
711 float const y2 = points.at(i + 1).y;
712
713 float const dx = x2 - x1;
714 float const dy = y2 - y1;
715 float const length = std::sqrt((dx * dx) + (dy * dy));
716
717 if (length < 0.001F) {
718 continue;
719 }
720
721 // Calculate perpendicular offset for line width
722 float const offsetX = (dy / length) * halfWidth;
723 float const offsetY = (dx / length) * halfWidth;
724
725 // Create quad vertices (two triangles)
726 // TODO: remove format off. this formatting is inconsistent between clang-format versions
727 // clang-format off
728 std::vector<SDL_Vertex> vertices = {
729 SDL_Vertex{
730 .position = {.x = x1 - offsetX, .y = y1 + offsetY},
731 .color =
732 {.r = static_cast<float>(mColor.r) / 255.0F,
733 .g = static_cast<float>(mColor.g) / 255.0F,
734 .b = static_cast<float>(mColor.b) / 255.0F,
735 .a = static_cast<float>(mColor.a) / 255.0F}},
736 SDL_Vertex{
737 .position = {.x = x1 + offsetX, .y = y1 - offsetY},
738 .color =
739 {.r = static_cast<float>(mColor.r) / 255.0F,
740 .g = static_cast<float>(mColor.g) / 255.0F,
741 .b = static_cast<float>(mColor.b) / 255.0F,
742 .a = static_cast<float>(mColor.a) / 255.0F}},
743 SDL_Vertex{
744 .position = {.x = x2 + offsetX, .y = y2 - offsetY},
745 .color =
746 {.r = static_cast<float>(mColor.r) / 255.0F,
747 .g = static_cast<float>(mColor.g) / 255.0F,
748 .b = static_cast<float>(mColor.b) / 255.0F,
749 .a = static_cast<float>(mColor.a) / 255.0F}},
750 SDL_Vertex{
751 .position = {.x = x2 - offsetX, .y = y2 + offsetY},
752 .color = {
753 .r = static_cast<float>(mColor.r) / 255.0F,
754 .g = static_cast<float>(mColor.g) / 255.0F,
755 .b = static_cast<float>(mColor.b) / 255.0F,
756 .a = static_cast<float>(mColor.a) / 255.0F}}};
757 // clang-format on
758
759 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
760 SDL_RenderGeometry(
762 nullptr,
763 vertices.data(),
764 static_cast<int>(vertices.size()),
765 indices.data(),
766 static_cast<int>(indices.size()));
767 }
768
770 }
771 }
772
773 void Graphics::drawPolyLine(PointVector const & points, 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
781 if (points.size() < 2) {
782 return;
783 }
784
785 ClipRectangle const & top = mClipStack.top();
786
787 // Convert points to SDL_FPoint array with offset applied
788 std::vector<SDL_FPoint> sdlPoints;
789 sdlPoints.reserve(points.size());
790 std::ranges::transform(points, std::back_inserter(sdlPoints), [&top](auto const & point) {
791 return SDL_FPoint{
792 .x = static_cast<float>(point.x + top.xOffset), .y = static_cast<float>(point.y + top.yOffset)};
793 });
794
795 if (width <= 1) {
796 // Use SDL_RenderLines for batched rendering of thin lines
798 setRenderDrawColor(mRenderTarget, mColor);
799 SDL_RenderLines(mRenderTarget, sdlPoints.data(), static_cast<int>(sdlPoints.size()));
801 } else {
802 // For thick lines, draw quads between each pair of points
804 setRenderDrawColor(mRenderTarget, mColor);
805
806 float const halfWidth = static_cast<float>(width) / 2.0F;
807
808 for (size_t i = 0; i < sdlPoints.size() - 1; ++i) {
809 float const x1 = sdlPoints.at(i).x;
810 float const y1 = sdlPoints.at(i).y;
811 float const x2 = sdlPoints.at(i + 1).x;
812 float const y2 = sdlPoints.at(i + 1).y;
813
814 float const dx = x2 - x1;
815 float const dy = y2 - y1;
816 float const length = std::sqrt((dx * dx) + (dy * dy));
817
818 if (length < 0.001F) {
819 continue;
820 }
821
822 // Calculate perpendicular offset for line width
823 float const offsetX = (dy / length) * halfWidth;
824 float const offsetY = (dx / length) * halfWidth;
825
826 // Create quad vertices (two triangles)
827 // TODO: remove format off. this formatting is inconsistent between clang-format versions
828 // clang-format off
829 std::vector<SDL_Vertex> vertices = {
830 SDL_Vertex{
831 .position = {.x = x1 - offsetX, .y = y1 + offsetY},
832 .color =
833 {.r = static_cast<float>(mColor.r) / 255.0F,
834 .g = static_cast<float>(mColor.g) / 255.0F,
835 .b = static_cast<float>(mColor.b) / 255.0F,
836 .a = static_cast<float>(mColor.a) / 255.0F}},
837 SDL_Vertex{
838 .position = {.x = x1 + offsetX, .y = y1 - offsetY},
839 .color =
840 {.r = static_cast<float>(mColor.r) / 255.0F,
841 .g = static_cast<float>(mColor.g) / 255.0F,
842 .b = static_cast<float>(mColor.b) / 255.0F,
843 .a = static_cast<float>(mColor.a) / 255.0F}},
844 SDL_Vertex{
845 .position = {.x = x2 + offsetX, .y = y2 - offsetY},
846 .color =
847 {.r = static_cast<float>(mColor.r) / 255.0F,
848 .g = static_cast<float>(mColor.g) / 255.0F,
849 .b = static_cast<float>(mColor.b) / 255.0F,
850 .a = static_cast<float>(mColor.a) / 255.0F}},
851 SDL_Vertex{
852 .position = {.x = x2 - offsetX, .y = y2 + offsetY},
853 .color = {
854 .r = static_cast<float>(mColor.r) / 255.0F,
855 .g = static_cast<float>(mColor.g) / 255.0F,
856 .b = static_cast<float>(mColor.b) / 255.0F,
857 .a = static_cast<float>(mColor.a) / 255.0F}}};
858 // clang-format on
859
860 std::array<int, 6> const indices = {0, 1, 2, 0, 2, 3};
861 SDL_RenderGeometry(
863 nullptr,
864 vertices.data(),
865 static_cast<int>(vertices.size()),
866 indices.data(),
867 static_cast<int>(indices.size()));
868 }
869
871 }
872 }
873
874 void Graphics::setColor(Color const & color)
875 {
876 mColor = color;
877
878 mAlpha = color.a != 255;
879 }
880
881 Color const & Graphics::getColor() const
882 {
883 return mColor;
884 }
885
886 std::shared_ptr<Font> Graphics::createFont(std::string const & filename, int size)
887 {
888 return std::make_shared<TrueTypeFont>(filename, size);
889 }
890
891 void Graphics::drawSDLTexture(SDL_Texture* texture, SDL_FRect source, SDL_FRect destination)
892 {
893 if (mClipStack.empty()) {
895 "Clip stack is empty, perhaps you"
896 "called a draw function outside of _beginDraw() and _endDraw()?");
897 }
898
899 ClipRectangle const & top = mClipStack.top();
900
901 destination.x += static_cast<float>(top.xOffset);
902 destination.y += static_cast<float>(top.yOffset);
903 destination.w = source.w;
904 destination.h = source.h;
905
906 SDL_RenderTexture(mRenderTarget, texture, &source, &destination);
907 }
908
910 {
911 SDL_GetRenderDrawColor(mRenderTarget, &r, &g, &b, &a);
912 }
913
915 {
916 SDL_SetRenderDrawColor(mRenderTarget, r, g, b, a);
917 }
918
919} // 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.