Skip to content

Commit 5da5521

Browse files
feat: add per-shape scaling and custom transformation matrices, with independent-axis ellipse and collider support
1 parent 422f76a commit 5da5521

11 files changed

Lines changed: 354 additions & 116 deletions

File tree

‎desktop/main.cpp‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,132 @@
44
#include "Shapes/Polygon.hpp"
55
#include "Shapes/Rectangle.hpp"
66
#include "Shapes/RegularPolygon.hpp"
7+
#include "Texture.hpp"
78
#include "Utils.hpp"
9+
#include <cassert>
810
#include <cstdio>
911
#include <memory>
1012

13+
static bool isWhite(const Display &d, int x, int y) {
14+
if (x < 0 || y < 0 || x >= d.width || y >= d.height)
15+
return false;
16+
const Color &c = d.pixels[y * d.width + x];
17+
return c.r > 200 && c.g > 200 && c.b > 200;
18+
}
19+
20+
static bool isRed(const Display &d, int x, int y) {
21+
if (x < 0 || y < 0 || x >= d.width || y >= d.height)
22+
return false;
23+
const Color &c = d.pixels[y * d.width + x];
24+
return c.r > 200 && c.g < 50 && c.b < 50;
25+
}
26+
27+
static void testScaleAndMatrix() {
28+
DrawOptions opts{200, 200, false};
29+
30+
{
31+
Display disp;
32+
disp.width = 200;
33+
disp.height = 200;
34+
disp.pixels.assign(200 * 200, Colors::BLACK);
35+
36+
Rectangle r(RectangleParams{10, 10, Colors::WHITE, 20, 20, false});
37+
r.setScale(2.0f, 3.0f, 0, 0);
38+
r.draw(disp, opts);
39+
40+
assert(isWhite(disp, 10 + (20 - 1) * 2, 10 + (20 - 1) * 3));
41+
assert(!isWhite(disp, 10 + (20 - 1), 10 + (20 - 1)));
42+
}
43+
44+
{
45+
Display disp;
46+
disp.width = 200;
47+
disp.height = 200;
48+
disp.pixels.assign(200 * 200, Colors::BLACK);
49+
50+
Rectangle r(RectangleParams{0, 0, Colors::WHITE, 5, 5, false});
51+
Matrix2D m;
52+
m.a = 1;
53+
m.b = 0;
54+
m.c = 0;
55+
m.d = 1;
56+
m.e = 100;
57+
m.f = 120;
58+
r.setTransformationMatrix(m);
59+
r.draw(disp, opts);
60+
61+
assert(isWhite(disp, 100, 120));
62+
assert(!isWhite(disp, 0, 0));
63+
}
64+
65+
{
66+
Circle c(CircleParams{0, 0, Colors::WHITE, 10});
67+
c.setScale(3.0f, 1.0f); // rx=30, ry=10
68+
c.addCollider();
69+
70+
PointCollider farOnX(25, 0);
71+
PointCollider farOnY(0, 25);
72+
assert(c.collider->intersects(&farOnX));
73+
assert(!c.collider->intersects(&farOnY));
74+
}
75+
76+
{
77+
Rectangle r(RectangleParams{0, 0, Colors::WHITE, 10, 10, false});
78+
r.addCollider();
79+
r.setScale(4.0f, 4.0f);
80+
PointCollider pt(35, 35);
81+
assert(r.collider->intersects(&pt));
82+
}
83+
84+
{
85+
Display disp;
86+
disp.width = 200;
87+
disp.height = 200;
88+
disp.pixels.assign(200 * 200, Colors::BLACK);
89+
90+
Circle c(CircleParams{100, 100, Colors::WHITE, 20});
91+
c.setScale(2.0f, 0.5f); // rx=40, ry=10
92+
c.draw(disp, opts);
93+
94+
assert(isWhite(disp, 140, 100));
95+
assert(!isWhite(disp, 100, 60));
96+
}
97+
98+
{
99+
std::vector<Color, PsramAllocator<Color>> texPixels(16, Colors::BLACK);
100+
texPixels[0] = Colors::RED; // marker at texel (0,0)
101+
Texture tex(texPixels, 4, 4);
102+
tex.setWrapMode("clamp");
103+
104+
Display disp;
105+
disp.width = 200;
106+
disp.height = 200;
107+
disp.pixels.assign(200 * 200, Colors::MAGENTA);
108+
109+
Rectangle r(RectangleParams{0, 0, Colors::WHITE, 4, 4, true});
110+
r.texture = &tex;
111+
r.fixTexture = true;
112+
113+
Matrix2D m;
114+
m.a = 1;
115+
m.b = 0;
116+
m.c = 0.3f; // skew
117+
m.d = 1;
118+
m.e = 50;
119+
m.f = 60;
120+
r.setTransformationMatrix(m);
121+
r.draw(disp, opts);
122+
123+
assert(isRed(disp, 50, 60));
124+
assert(!isRed(disp, 51, 60));
125+
}
126+
127+
printf("scale/matrix/collider self-check passed\n");
128+
}
129+
11130
int main() {
131+
testScaleAndMatrix();
132+
12133
const int W = 64, H = 64;
13134

14135
Renderer renderer(W, H);

‎include/Collider.hpp‎

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class Collider : public Collidable {
4545
float x;
4646
float y;
4747
float rotation;
48+
float scaleX = 1.0f;
49+
float scaleY = 1.0f;
4850

4951
Collider(float x, float y) : x(x), y(y), rotation(0.0f) {}
5052
virtual ~Collider() = default;
@@ -62,7 +64,6 @@ class Collider : public Collidable {
6264
}
6365

6466
virtual void rotate(float angleDegrees) { rotation += angleDegrees; }
65-
6667
};
6768

6869
namespace CollisionMath {
@@ -108,6 +109,15 @@ inline bool pointInPolygon(int x, int y,
108109
return inside;
109110
}
110111

112+
inline bool pointInEllipse(float px, float py, float cx, float cy, float rx,
113+
float ry) {
114+
if (rx <= 0.0f || ry <= 0.0f)
115+
return false;
116+
float dx = (px - cx) / rx;
117+
float dy = (py - cy) / ry;
118+
return dx * dx + dy * dy <= 1.0f;
119+
}
120+
111121
inline bool lineIntersectLine(int x1, int y1, int x2, int y2, int x3, int y3,
112122
int x4, int y4) {
113123
int denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
@@ -121,7 +131,7 @@ inline bool lineIntersectLine(int x1, int y1, int x2, int y2, int x3, int y3,
121131

122132
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
123133
}
124-
}
134+
} // namespace CollisionMath
125135

126136
class IntersectionVisitor : public CollisionVisitor {
127137
private:
@@ -195,6 +205,9 @@ class CircleCollider : public Collider {
195205
IntersectionVisitor visitor(other);
196206
return this->accept(&visitor);
197207
}
208+
209+
float radiusX() const { return radius * scaleX; }
210+
float radiusY() const { return radius * scaleY; }
198211
};
199212

200213
class RectangleCollider : public Collider {
@@ -214,17 +227,20 @@ class RectangleCollider : public Collider {
214227
return this->accept(&visitor);
215228
}
216229

230+
float scaledWidth() const { return width * scaleX; }
231+
float scaledHeight() const { return height * scaleY; }
232+
217233
std::vector<std::pair<int, int>> getCorners() const {
218234
std::vector<std::pair<int, int>> corners;
219235
float rads = rotation * CollisionMath::DEG_TO_RAD;
236+
float w = scaledWidth();
237+
float h = scaledHeight();
220238

221239
corners.push_back({(int)x, (int)y});
222240

223-
corners.push_back(CollisionMath::rotatePoint(x + width, y, x, y, rads));
224-
corners.push_back(
225-
CollisionMath::rotatePoint(x + width, y + height, x, y, rads));
226-
corners.push_back(
227-
CollisionMath::rotatePoint(x, y + height, x, y, rads));
241+
corners.push_back(CollisionMath::rotatePoint(x + w, y, x, y, rads));
242+
corners.push_back(CollisionMath::rotatePoint(x + w, y + h, x, y, rads));
243+
corners.push_back(CollisionMath::rotatePoint(x, y + h, x, y, rads));
228244

229245
return corners;
230246
}
@@ -254,8 +270,10 @@ class PolygonCollider : public Collider {
254270
float s = std::sin(rads);
255271

256272
for (const auto &p : points) {
257-
int rx = static_cast<int>(p.first * c + p.second * s);
258-
int ry = static_cast<int>(-p.first * s + p.second * c);
273+
float sx = p.first * scaleX;
274+
float sy = p.second * scaleY;
275+
int rx = static_cast<int>(sx * c + sy * s);
276+
int ry = static_cast<int>(-sx * s + sy * c);
259277
worldPoints.push_back({rx + (int)x, ry + (int)y});
260278
}
261279
return worldPoints;
@@ -279,10 +297,13 @@ class LineSegmentCollider : public Collider {
279297
return this->accept(&visitor);
280298
}
281299
std::pair<int, int> getP2() const {
300+
float scaledX2 = x + (x2 - x) * scaleX;
301+
float scaledY2 = y + (y2 - y) * scaleY;
282302
if (rotation == 0.0f)
283-
return {x2, y2};
303+
return {static_cast<int>(scaledX2), static_cast<int>(scaledY2)};
284304
float rads = rotation * CollisionMath::DEG_TO_RAD;
285-
return CollisionMath::rotatePoint(x2, y2, x, y, rads);
305+
return CollisionMath::rotatePoint(
306+
static_cast<int>(scaledX2), static_cast<int>(scaledY2), x, y, rads);
286307
}
287308
};
288309

‎include/Shapes/Circle.hpp‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ class Circle : public Shape {
2727
private:
2828
void drawHorizontalLine(Display &displayGrid, int x1, int x2, int y,
2929
const PaintCtx &ctx);
30-
void drawAntiAliasedPoint(Display &displayGrid, int cx, int cy, int x,
31-
int y, float intensity, const PaintCtx &ctx);
32-
void fillCircle(Display &displayGrid, int cx, int cy, int r,
30+
void drawEllipsePoints(Display &displayGrid, int cx, int cy, int x, int y,
31+
float intensity, const PaintCtx &ctx);
32+
void fillCircle(Display &displayGrid, int cx, int cy, float rx, float ry,
3333
const PaintCtx &ctx);
3434
};

‎include/Shapes/Shape.hpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "../Texture.hpp"
55
#include "Utils.hpp"
66
#include <memory>
7+
#include <optional>
78
#include <vector>
89

910
struct DrawOptions {
@@ -71,6 +72,8 @@ class Shape {
7172
Matrix2D _cachedGlobalMatrix;
7273
bool _isDirty = true;
7374

75+
std::optional<Matrix2D> _customMatrix;
76+
7477
std::pair<int, int> getTransformedPosition(int inputX, int inputY);
7578
PaintCtx makePaintCtx(const Color &color) const;
7679

@@ -112,6 +115,15 @@ class Shape {
112115
void scaleY(float scaleY, float originY = -1);
113116
void setScaleOrigin(int x, int y);
114117

118+
void setTransformationMatrix(const Matrix2D &matrix) {
119+
_customMatrix = matrix;
120+
markDirty();
121+
}
122+
void clearTransformationMatrix() {
123+
_customMatrix.reset();
124+
markDirty();
125+
}
126+
115127
void addCollider(std::unique_ptr<Collider> newCollider = nullptr);
116128
void removeCollider();
117129
bool intersects(const std::shared_ptr<Shape> &other);

‎src/Collider.cpp‎

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ getRegularPolyVertices(const RegularPolygonCollider *rp) {
2020
for (int i = 0; i < sides; i++) {
2121
float angle = i * angleStep + startAngle;
2222

23-
float localX = rp->radius * std::cos(angle);
24-
float localY = rp->radius * std::sin(angle);
23+
float localX = rp->radius * std::cos(angle) * rp->scaleX;
24+
float localY = rp->radius * std::sin(angle) * rp->scaleY;
2525

2626
int finalX = static_cast<int>(localX * c + localY * s) + rp->x;
2727
int finalY = static_cast<int>(-localX * s + localY * c) + rp->y;
@@ -202,32 +202,33 @@ bool IntersectionVisitor::visitRegularPolygon(
202202

203203
bool IntersectionVisitor::circleCircle(const CircleCollider *c1,
204204
const CircleCollider *c2) {
205-
float distSq = CollisionMath::distanceSquared(c1->x, c1->y, c2->x, c2->y);
206-
float radSum = c1->radius + c2->radius;
207-
return distSq < radSum * radSum;
205+
return CollisionMath::pointInEllipse(c2->x, c2->y, c1->x, c1->y,
206+
c1->radiusX() + c2->radiusX(),
207+
c1->radiusY() + c2->radiusY());
208208
}
209209

210210
bool IntersectionVisitor::circleRectangle(const CircleCollider *circle,
211211
const RectangleCollider *rect) {
212212
if (rect->rotation == 0.0f) {
213213
// Fast AABB Check
214+
float w = rect->scaledWidth();
215+
float h = rect->scaledHeight();
214216
float testX = circle->x;
215217
float testY = circle->y;
216218

217219
if (circle->x < rect->x)
218220
testX = rect->x;
219-
else if (circle->x > rect->x + rect->width)
220-
testX = rect->x + rect->width;
221+
else if (circle->x > rect->x + w)
222+
testX = rect->x + w;
221223

222224
if (circle->y < rect->y)
223225
testY = rect->y;
224-
else if (circle->y > rect->y + rect->height)
225-
testY = rect->y + rect->height;
226+
else if (circle->y > rect->y + h)
227+
testY = rect->y + h;
226228

227-
float distX = circle->x - testX;
228-
float distY = circle->y - testY;
229-
return (distX * distX + distY * distY) <=
230-
(circle->radius * circle->radius);
229+
return CollisionMath::pointInEllipse(testX, testY, circle->x, circle->y,
230+
circle->radiusX(),
231+
circle->radiusY());
231232
} else {
232233
// Rotated Rectangle -> Treat as Polygon vs Circle
233234
auto corners = rect->getCorners();
@@ -247,9 +248,9 @@ bool IntersectionVisitor::circleRectangle(const CircleCollider *circle,
247248

248249
bool IntersectionVisitor::circlePoint(const CircleCollider *circle,
249250
const PointCollider *point) {
250-
return CollisionMath::distanceSquared(circle->x, circle->y, point->x,
251-
point->y) <=
252-
(circle->radius * circle->radius);
251+
return CollisionMath::pointInEllipse(point->x, point->y, circle->x,
252+
circle->y, circle->radiusX(),
253+
circle->radiusY());
253254
}
254255

255256
bool IntersectionVisitor::circleLine(const CircleCollider *circle,
@@ -269,9 +270,9 @@ bool IntersectionVisitor::circleLine(const CircleCollider *circle,
269270
lineLenSq));
270271
float closestX = line->x + t * (x2 - line->x);
271272
float closestY = line->y + t * (y2 - line->y);
272-
return CollisionMath::distanceSquared(circle->x, circle->y, closestX,
273-
closestY) <
274-
(circle->radius * circle->radius);
273+
return CollisionMath::pointInEllipse(closestX, closestY, circle->x,
274+
circle->y, circle->radiusX(),
275+
circle->radiusY());
275276
}
276277

277278
bool IntersectionVisitor::circlePolygon(const CircleCollider *circle,
@@ -311,8 +312,10 @@ bool IntersectionVisitor::rectangleRectangle(const RectangleCollider *r1,
311312
const RectangleCollider *r2) {
312313
// Optimization: If BOTH are unrotated, use fast AABB
313314
if (r1->rotation == 0.0f && r2->rotation == 0.0f) {
314-
return !(r1->x >= r2->x + r2->width || r1->x + r1->width <= r2->x ||
315-
r1->y >= r2->y + r2->height || r1->y + r1->height <= r2->y);
315+
float w1 = r1->scaledWidth(), h1 = r1->scaledHeight();
316+
float w2 = r2->scaledWidth(), h2 = r2->scaledHeight();
317+
return !(r1->x >= r2->x + w2 || r1->x + w1 <= r2->x ||
318+
r1->y >= r2->y + h2 || r1->y + h1 <= r2->y);
316319
}
317320

318321
auto p1 = r1->getCorners();
@@ -323,8 +326,10 @@ bool IntersectionVisitor::rectangleRectangle(const RectangleCollider *r1,
323326
bool IntersectionVisitor::rectanglePoint(const RectangleCollider *rect,
324327
const PointCollider *point) {
325328
if (rect->rotation == 0.0f) {
326-
return point->x >= rect->x && point->x <= rect->x + rect->width &&
327-
point->y >= rect->y && point->y <= rect->y + rect->height;
329+
return point->x >= rect->x &&
330+
point->x <= rect->x + rect->scaledWidth() &&
331+
point->y >= rect->y &&
332+
point->y <= rect->y + rect->scaledHeight();
328333
}
329334
auto corners = rect->getCorners();
330335
return CollisionMath::pointInPolygon(point->x, point->y, corners);

0 commit comments

Comments
 (0)