TinyGPU
Loading...
Searching...
No Matches
SpriteInfo.h
Go to the documentation of this file.
1#pragma once
2#include <algorithm>
3#include <cmath>
4#include <memory>
5
6#include "TinyGPU/Color/RGB565.h"
7#include "TinyGPU/ThreeD/Vector.h"
8#include "TinyGPU/Input/TouchDriverCommon.h"
9
10namespace tinygpu {
11
12/**
13 * @brief Represents a rectangular area on the screen or surface.
14 *
15 * The Rect structure is used to define a rectangle by its top-left corner
16 * coordinates (x, y) and its dimensions (width, height). It is commonly used
17 * for sprite placement, collision detection, and drawing operations.
18 */
19struct Rect {
20 size_t x = 0;
21 size_t y = 0;
24
25 /// Returns the intersection rectangle of two rectangles.
26 static Rect intersect(const Rect& first, const Rect& second) {
27 const size_t overlapX = std::max(first.x, second.x);
28 const size_t overlapY = std::max(first.y, second.y);
29 const size_t firstRight = first.x + first.width;
30 const size_t firstBottom = first.y + first.height;
31 const size_t secondRight = second.x + second.width;
32 const size_t secondBottom = second.y + second.height;
33 const size_t overlapRight = std::min(firstRight, secondRight);
34 const size_t overlapBottom = std::min(firstBottom, secondBottom);
35
36 if (overlapX >= overlapRight || overlapY >= overlapBottom) {
37 return {};
38 }
39
40 return {overlapX, overlapY, overlapRight - overlapX,
41 overlapBottom - overlapY};
42 }
43};
44
45/**
46 * Tracks a sprite instance together with its saved background pixels.
47 *
48 * Each SpriteInfo stores the sprite position, transparent color, current
49 * sprite image, and a snapshot of the framebuffer region covered by it.
50 */
51template <typename RGB_T = RGB565, typename SurfaceT = Surface<RGB_T>>
52struct SpriteInfo {
53 size_t x = 0;
54 size_t y = 0;
57 RGB_T invisibleColor = RGB_T(0);
58 const ISurface<RGB_T>* sprite = nullptr;
59 bool isSurfaceAutoDelete = false;
61 // True once transformedSprite actually holds a scaled/rotated image (set by
62 // setTransformedSprite()) rather than just the blank buffer setMaxSize()
63 // preallocates - currentSprite() must not treat that blank buffer as "the"
64 // sprite image, or the original sprite never gets drawn until the first
65 // scaleSprite()/rotateSprite() call.
67 // Last scale/rotation requested via scaleSprite()/rotateSprite(). Both
68 // are always re-rendered from the pristine `sprite` image using these
69 // two values together (see renderTransformedSprite() below) - never by
70 // re-scaling/re-rotating the already-transformed `transformedSprite`
71 // raster, which would compound nearest-neighbor resampling artifacts
72 // (no interpolation) frame after frame until the image degenerates into
73 // a solid blob within seconds.
74 float currentScale = 1.0f;
75 float currentAngleDegrees = 0.0f;
78 void (*onTouchCallback)(SpriteInfo&, Point) = nullptr;
79
80 SpriteInfo(size_t startX, size_t startY, const ISurface<RGB_T>& sourceSprite,
81 RGB_T transparentColor, IFont<RGB_T>& font)
82 : x(startX),
83 y(startY),
84 invisibleColor(transparentColor),
87 fontRef(font) {}
88
89 virtual ~SpriteInfo() {
90 if (isSurfaceAutoDelete && sprite) {
91 delete sprite;
92 }
93 }
94
95 Rect getRect() const {
96 return {x, y, currentSprite().width(), currentSprite().height()};
97 }
98
99 /// Returns the size a transformed image of (rawWidth, rawHeight) will
100 /// actually end up stored/drawn at by setTransformedSprite() below. Once
101 /// a fixed buffer exists (from setMaxSize() or an earlier
102 /// setTransformedSprite() call), the drawn size is ALWAYS exactly
103 /// maxWidth x maxHeight - never the raw image's own size - because
104 /// setTransformedSprite() pads a smaller image with invisibleColor
105 /// rather than shrinking the buffer to fit it (e.g. scaling down) and
106 /// crops a larger one down to the buffer (e.g. rotating a square grows
107 /// its bounding box). Only when no fixed buffer exists yet does the
108 /// drawn size match the raw image exactly (setTransformedSprite()
109 /// allocates the very first buffer to fit it precisely).
110 ///
111 /// Callers computing where to anchor/redraw a transformed sprite (see
112 /// FrameBuffer::applyTransformedSprite(), SpriteDisplay::
113 /// applyTransformedSprite()) must use this size, not the raw transformed
114 /// image's own size, or their background restore/capture bounds won't
115 /// match the region setTransformedSprite() + drawSprite() actually
116 /// touch - leaving stale, un-restored pixels on screen (visible as a
117 /// second, "ghost" copy of the sprite trailing the real one).
118 Rect clampedSize(size_t rawWidth, size_t rawHeight) const {
119 if (transformedSprite) {
120 return {0, 0, maxWidth, maxHeight};
121 }
122 return {0, 0, rawWidth, rawHeight};
123 }
124
125 /// Returns the sprite image currently used for drawing.
126 const ISurface<RGB_T>& currentSprite() const {
127 return hasTransformedContent
128 ? static_cast<const ISurface<RGB_T>&>(*transformedSprite)
129 : *sprite;
130 }
131
132 /// Saves the background pixels currently covered by the sprite.
133 virtual void saveOriginalPixels(ISurface<RGB_T>& framebuffer) {
134 originalPixels.resize(currentSprite().width(), currentSprite().height());
135 framebuffer.copySprite(x, y, originalPixels);
136 }
137
138 void onTouch(Point point) {
139 if (onTouchCallback) {
140 onTouchCallback(*this, point);
141 }
142 }
143
144 /// Set the maximum buffer size for transformedSprite and allocate buffer.
145 void setMaxSize(size_t maxX, size_t maxY) {
146 maxWidth = maxX;
147 maxHeight = maxY;
148 if (!transformedSprite) {
149 transformedSprite =
150 std::make_unique<SurfaceT>(maxWidth, maxHeight, fontRef);
151 transformedSprite->begin();
152 }
153 }
154
155 /// Replaces the current transformed sprite image, using only the allocated
156 /// buffer.
157 void setTransformedSprite(SurfaceT&& newSprite) {
158 if (!transformedSprite) {
159 // If max size not set, use current size as max
160 maxWidth = newSprite.width();
161 maxHeight = newSprite.height();
162 transformedSprite =
163 std::make_unique<SurfaceT>(maxWidth, maxHeight, newSprite.font());
164 transformedSprite->begin();
165 }
166 // Only copy the region that fits, centered in both the source and the
167 // destination buffer - a top-left crop/pad would visually shift the
168 // sprite toward the top-left corner whenever newSprite is larger or
169 // smaller than the fixed buffer (e.g. a rotated image's larger
170 // bounding box), which would then disagree with the centered anchor
171 // position applyTransformedSprite() computed for it via
172 // SpriteInfo::clampedSize() above.
173 const size_t copyWidth = std::min(maxWidth, newSprite.width());
174 const size_t copyHeight = std::min(maxHeight, newSprite.height());
175 const size_t srcOffsetX = (newSprite.width() - copyWidth) / 2;
176 const size_t srcOffsetY = (newSprite.height() - copyHeight) / 2;
177 const size_t dstOffsetX = (maxWidth - copyWidth) / 2;
178 const size_t dstOffsetY = (maxHeight - copyHeight) / 2;
179
180 transformedSprite->clear(RGB_T(0));
181 for (size_t y = 0; y < copyHeight; ++y) {
182 for (size_t x = 0; x < copyWidth; ++x) {
183 transformedSprite->setPixel(
184 dstOffsetX + x, dstOffsetY + y,
185 newSprite.getPixel(srcOffsetX + x, srcOffsetY + y));
186 }
187 }
189 }
190};
191
192/**
193 * A specialized SpriteInfo that does not save the background pixels.
194 *
195 * This is useful for sprites that do not need to restore the background,
196 * such as those that are always drawn on a solid color or when the
197 * background is managed separately.
198 */
199template <typename RGB_T = RGB565, typename SurfaceT = Surface<RGB_T>>
200struct SpriteInfoWithoutBackground : public SpriteInfo<RGB_T, SurfaceT> {
201 SpriteInfoWithoutBackground(size_t startX, size_t startY,
202 const ISurface<RGB_T>& sourceSprite,
203 RGB_T transparentColor, IFont<RGB_T>& font)
206
207 // Do nothing; no background saving for this sprite type
209};
210
211/// Finds the iterator to a sprite within a sprite list, shared by
212/// FrameBuffer and SpriteDisplay.
213template <typename RGB_T, typename SurfaceT>
217 return std::find_if(
218 sprites.begin(), sprites.end(),
219 [&spriteInfo](const std::unique_ptr<SpriteInfo<RGB_T, SurfaceT>>& entry) {
220 return entry.get() == &spriteInfo;
221 });
222}
223
224/// Returns a scaled copy of a sprite image, shared by FrameBuffer and
225/// SpriteDisplay.
226template <typename RGB_T>
228 float scale, IFont<RGB_T>& font) {
229 size_t scaledWidth = static_cast<size_t>(source.width() * scale);
230 size_t scaledHeight = static_cast<size_t>(source.height() * scale);
231
232 if (scaledWidth == 0) {
233 scaledWidth = 1;
234 }
235 if (scaledHeight == 0) {
236 scaledHeight = 1;
237 }
238
239 Surface<RGB_T> scaledSprite(scaledWidth, scaledHeight, font);
240 scaledSprite.begin();
241 for (size_t currentY = 0; currentY < scaledHeight; ++currentY) {
242 const size_t sourceY =
243 static_cast<size_t>(static_cast<float>(currentY) / scale);
244 const size_t clampedY =
245 sourceY < source.height() ? sourceY : source.height() - 1;
246 for (size_t currentX = 0; currentX < scaledWidth; ++currentX) {
247 const size_t sourceX =
248 static_cast<size_t>(static_cast<float>(currentX) / scale);
249 const size_t clampedX =
250 sourceX < source.width() ? sourceX : source.width() - 1;
251 scaledSprite.setPixel(currentX, currentY,
252 source.getPixel(clampedX, clampedY));
253 }
254 }
255
256 return scaledSprite;
257}
258
259/// Returns a rotated copy of a sprite image, filling empty space with
260/// fillColor. Shared by FrameBuffer and SpriteDisplay.
261template <typename RGB_T>
264 IFont<RGB_T>& font) {
265 const float radians =
266 static_cast<float>(angleDegrees) * 3.14159265358979323846 / 180.0;
267 const float cosine = std::cos(radians);
268 const float sine = std::sin(radians);
269 const float sourceWidth = static_cast<float>(source.width());
270 const float sourceHeight = static_cast<float>(source.height());
271 size_t rotatedWidth = static_cast<size_t>(std::ceil(
272 std::fabs(sourceWidth * cosine) + std::fabs(sourceHeight * sine)));
273 size_t rotatedHeight = static_cast<size_t>(std::ceil(
274 std::fabs(sourceWidth * sine) + std::fabs(sourceHeight * cosine)));
275
276 if (rotatedWidth == 0) {
277 rotatedWidth = 1;
278 }
279 if (rotatedHeight == 0) {
280 rotatedHeight = 1;
281 }
282
283 Surface<RGB_T> rotatedSprite(rotatedWidth, rotatedHeight, font);
284 rotatedSprite.begin();
285 rotatedSprite.clear(fillColor);
286
287 const float sourceCenterX = (sourceWidth - 1.0) / 2.0;
288 const float sourceCenterY = (sourceHeight - 1.0) / 2.0;
289 const float rotatedCenterX = (static_cast<float>(rotatedWidth) - 1.0) / 2.0;
290 const float rotatedCenterY =
291 (static_cast<float>(rotatedHeight) - 1.0) / 2.0;
292
293 for (size_t currentY = 0; currentY < rotatedHeight; ++currentY) {
294 for (size_t currentX = 0; currentX < rotatedWidth; ++currentX) {
295 const float targetX = static_cast<float>(currentX) - rotatedCenterX;
296 const float targetY = static_cast<float>(currentY) - rotatedCenterY;
297 const float sourceX =
298 (targetX * cosine) + (targetY * sine) + sourceCenterX;
299 const float sourceY =
300 (-targetX * sine) + (targetY * cosine) + sourceCenterY;
301 const long nearestX = std::lround(sourceX);
302 const long nearestY = std::lround(sourceY);
303
304 if (nearestX >= 0 && nearestY >= 0 &&
305 static_cast<size_t>(nearestX) < source.width() &&
306 static_cast<size_t>(nearestY) < source.height()) {
307 rotatedSprite.setPixel(
308 currentX, currentY,
309 source.getPixel(static_cast<size_t>(nearestX),
310 static_cast<size_t>(nearestY)));
311 }
312 }
313 }
314
315 return rotatedSprite;
316}
317
318/// Renders spriteInfo's pristine `sprite` image at its currently requested
319/// scale and rotation (SpriteInfo::currentScale/currentAngleDegrees, set by
320/// scaleSprite()/rotateSprite() - see FrameBuffer.h/SpriteDisplay.h) in one
321/// combined pass, shared by FrameBuffer and SpriteDisplay.
322///
323/// This always starts over from the pristine original, never from
324/// spriteInfo.currentSprite() (the previous frame's already-transformed
325/// raster) - scaleSpriteImage()/rotateSpriteImage() use nearest-neighbor
326/// sampling with no interpolation, so repeatedly transforming an
327/// already-transformed raster compounds resampling artifacts frame after
328/// frame until the image degenerates into a solid blob within seconds
329/// (confirmed empirically: an 18x18 circle sprite scaled/rotated every
330/// frame that way fills its entire 40x40 max buffer within ~10 frames).
331/// Re-deriving from the pristine source every time is also the only way to
332/// support an absolute scale/angle contract at all - scaleSprite(info, 1.0)
333/// must be able to undo a previous scaleSprite(info, 1.3), which is
334/// impossible if 1.3x has already been baked irreversibly into the stored
335/// raster.
336template <typename RGB_T, typename SurfaceT>
339 Surface<RGB_T> scaled =
340 scaleSpriteImage(*spriteInfo.sprite, spriteInfo.currentScale, font);
341 return rotateSpriteImage(scaled, spriteInfo.currentAngleDegrees,
342 spriteInfo.invisibleColor, font);
343}
344
345} // namespace tinygpu
Definition: DSIBusESP32.h:19
Surface< RGB_T > scaleSpriteImage(const ISurface< RGB_T > &source, float scale, IFont< RGB_T > &font)
Definition: SpriteInfo.h:227
Surface< RGB_T > rotateSpriteImage(const ISurface< RGB_T > &source, float angleDegrees, RGB_T fillColor, IFont< RGB_T > &font)
Definition: SpriteInfo.h:262
Surface< RGB_T > renderTransformedSprite(const SpriteInfo< RGB_T, SurfaceT > &spriteInfo, IFont< RGB_T > &font)
Definition: SpriteInfo.h:337
Vector< std::unique_ptr< SpriteInfo< RGB_T, SurfaceT > > >::iterator findSprite(Vector< std::unique_ptr< SpriteInfo< RGB_T, SurfaceT > > > &sprites, SpriteInfo< RGB_T, SurfaceT > &spriteInfo)
Definition: SpriteInfo.h:215
Touch point.
Definition: TouchDriverCommon.h:32
Represents a rectangular area on the screen or surface.
Definition: SpriteInfo.h:19
size_t height
Definition: SpriteInfo.h:23
size_t width
Definition: SpriteInfo.h:22
size_t y
Definition: SpriteInfo.h:21
size_t x
Definition: SpriteInfo.h:20
static Rect intersect(const Rect &first, const Rect &second)
Returns the intersection rectangle of two rectangles.
Definition: SpriteInfo.h:26
Definition: SpriteInfo.h:200
SpriteInfoWithoutBackground(size_t startX, size_t startY, const ISurface< RGB_T > &sourceSprite, RGB_T transparentColor, IFont< RGB_T > &font)
Definition: SpriteInfo.h:201
void saveOriginalPixels(ISurface< RGB_T > &) override
Saves the background pixels currently covered by the sprite.
Definition: SpriteInfo.h:208
Definition: SpriteInfo.h:52
bool isSurfaceAutoDelete
Definition: SpriteInfo.h:59
size_t y
Definition: SpriteInfo.h:54
size_t maxWidth
Definition: SpriteInfo.h:55
std::unique_ptr< SurfaceT > transformedSprite
Definition: SpriteInfo.h:60
float currentAngleDegrees
Definition: SpriteInfo.h:75
void setMaxSize(size_t maxX, size_t maxY)
Set the maximum buffer size for transformedSprite and allocate buffer.
Definition: SpriteInfo.h:145
void onTouch(Point point)
Definition: SpriteInfo.h:138
virtual void saveOriginalPixels(ISurface< RGB_T > &framebuffer)
Saves the background pixels currently covered by the sprite.
Definition: SpriteInfo.h:133
Rect clampedSize(size_t rawWidth, size_t rawHeight) const
Definition: SpriteInfo.h:118
size_t x
Definition: SpriteInfo.h:53
virtual ~SpriteInfo()
Definition: SpriteInfo.h:89
void(* onTouchCallback)(SpriteInfo &, Point)
Definition: SpriteInfo.h:78
IFont< RGB_T > & fontRef
Definition: SpriteInfo.h:77
SpriteInfo(size_t startX, size_t startY, const ISurface< RGB_T > &sourceSprite, RGB_T transparentColor, IFont< RGB_T > &font)
Definition: SpriteInfo.h:80
Rect getRect() const
Definition: SpriteInfo.h:95
void setTransformedSprite(SurfaceT &&newSprite)
Definition: SpriteInfo.h:157
const ISurface< RGB_T > & currentSprite() const
Returns the sprite image currently used for drawing.
Definition: SpriteInfo.h:126
SurfaceT originalPixels
Definition: SpriteInfo.h:76
RGB_T invisibleColor
Definition: SpriteInfo.h:57
bool hasTransformedContent
Definition: SpriteInfo.h:66
const ISurface< RGB_T > * sprite
Definition: SpriteInfo.h:58
float currentScale
Definition: SpriteInfo.h:74
size_t maxHeight
Definition: SpriteInfo.h:56