Implement Shape rendering and Transform math for SDL2 backend

Shape rendering now works:
- Shape::draw() generates triangle vertices for fill and outline
- RectangleShape, CircleShape, ConvexShape provide getPointCount()/getPoint()
- Shapes render with correct fill color, outline color, and outline thickness

Transform class fully implemented:
- translate(), rotate(), scale() modify the 3x3 affine matrix
- transformPoint() applies transform to Vector2f
- operator* combines transforms
- getInverse() computes inverse transform

Transformable::getTransform() now computes proper transform from:
- position, rotation, scale, and origin

RenderStates now has transform, blendMode, shader members

Canvas sizing fixed for Emscripten:
- EM_ASM sets canvas size after SDL window creation
- SDL_GL_MakeCurrent called after canvas resize

Result: RectangleShape UI elements render in correct positions!

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-31 11:58:42 -05:00
commit a702d3cab4
2 changed files with 303 additions and 26 deletions

View file

@ -370,9 +370,16 @@ void SDL2Renderer::setProjection(float left, float right, float bottom, float to
projectionMatrix_[15] = 1.0f; projectionMatrix_[15] = 1.0f;
} }
static int clearCount = 0;
void SDL2Renderer::clear(float r, float g, float b, float a) { void SDL2Renderer::clear(float r, float g, float b, float a) {
glClearColor(r, g, b, a); glClearColor(r, g, b, a);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
// Debug: Log first few clears to confirm render loop is running
if (clearCount < 5) {
std::cout << "SDL2Renderer::clear(" << r << ", " << g << ", " << b << ", " << a << ") #" << clearCount << std::endl;
clearCount++;
}
} }
void SDL2Renderer::drawTriangles(const float* vertices, size_t vertexCount, void SDL2Renderer::drawTriangles(const float* vertices, size_t vertexCount,
@ -461,9 +468,14 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
} }
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
// For Emscripten, we need to set the canvas size explicitly // For Emscripten, tell SDL2 which canvas element to use
// The canvas element with id="canvas" is used by default // SDL_HINT_EMSCRIPTEN_CANVAS_SELECTOR = "SDL_EMSCRIPTEN_CANVAS_SELECTOR"
SDL_SetHint("SDL_EMSCRIPTEN_CANVAS_SELECTOR", "#canvas");
// Set the canvas size explicitly before creating the window
emscripten_set_canvas_element_size("#canvas", mode.width, mode.height); emscripten_set_canvas_element_size("#canvas", mode.width, mode.height);
std::cout << "Emscripten: Setting canvas to " << mode.width << "x" << mode.height << std::endl;
#endif #endif
// Create window // Create window
@ -493,6 +505,28 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
title_ = title; title_ = title;
open_ = true; open_ = true;
#ifdef __EMSCRIPTEN__
// Force canvas size AFTER SDL window creation (SDL may have reset it)
emscripten_set_canvas_element_size("#canvas", mode.width, mode.height);
// Also set the CSS size to match
EM_ASM({
var canvas = document.getElementById('canvas');
if (canvas) {
canvas.width = $0;
canvas.height = $1;
canvas.style.width = $0 + 'px';
canvas.style.height = $1 + 'px';
console.log('EM_ASM: Set canvas to ' + $0 + 'x' + $1);
} else {
console.error('EM_ASM: Canvas element not found!');
}
}, mode.width, mode.height);
// Re-make context current after canvas resize
SDL_GL_MakeCurrent(window, context);
#endif
// Initialize OpenGL resources now that we have a context // Initialize OpenGL resources now that we have a context
if (!SDL2Renderer::getInstance().initGL()) { if (!SDL2Renderer::getInstance().initGL()) {
std::cerr << "RenderWindow: Failed to initialize OpenGL resources" << std::endl; std::cerr << "RenderWindow: Failed to initialize OpenGL resources" << std::endl;
@ -504,6 +538,13 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
// Set up OpenGL state // Set up OpenGL state
glViewport(0, 0, mode.width, mode.height); glViewport(0, 0, mode.width, mode.height);
std::cout << "GL viewport set to " << mode.width << "x" << mode.height << std::endl;
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
std::cerr << "GL error after viewport: " << err << std::endl;
}
SDL2Renderer::getInstance().setProjection(0, mode.width, mode.height, 0); SDL2Renderer::getInstance().setProjection(0, mode.width, mode.height, 0);
// Enable blending for transparency // Enable blending for transparency
@ -513,9 +554,21 @@ void RenderWindow::create(VideoMode mode, const std::string& title, uint32_t sty
// Initial clear to a visible color to confirm GL is working // Initial clear to a visible color to confirm GL is working
glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray glClearColor(0.2f, 0.3f, 0.4f, 1.0f); // Blue-gray
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
err = glGetError();
if (err != GL_NO_ERROR) {
std::cerr << "GL error after clear: " << err << std::endl;
}
SDL_GL_SwapWindow(window); SDL_GL_SwapWindow(window);
err = glGetError();
if (err != GL_NO_ERROR) {
std::cerr << "GL error after swap: " << err << std::endl;
}
std::cout << "RenderWindow: Created " << mode.width << "x" << mode.height << " window" << std::endl; std::cout << "RenderWindow: Created " << mode.width << "x" << mode.height << " window" << std::endl;
std::cout << "WebGL context should now show blue-gray" << std::endl;
} }
void RenderWindow::close() { void RenderWindow::close() {
@ -1118,7 +1171,125 @@ bool Font::loadFromMemory(const void* data, size_t sizeInBytes) {
// ============================================================================= // =============================================================================
void Shape::draw(RenderTarget& target, RenderStates states) const { void Shape::draw(RenderTarget& target, RenderStates states) const {
// TODO: Generate vertices and draw using SDL2Renderer size_t pointCount = getPointCount();
if (pointCount < 3) return;
// Get the combined transform
Transform combinedTransform = states.transform * getTransform();
// Build vertex data for fill (triangle fan from center)
std::vector<float> vertices;
std::vector<float> colors;
// Calculate center point
Vector2f center(0, 0);
for (size_t i = 0; i < pointCount; ++i) {
center.x += getPoint(i).x;
center.y += getPoint(i).y;
}
center.x /= pointCount;
center.y /= pointCount;
// Transform center
Vector2f transformedCenter = combinedTransform.transformPoint(center);
// Build triangles (fan from center)
Color fill = getFillColor();
float fr = fill.r / 255.0f;
float fg = fill.g / 255.0f;
float fb = fill.b / 255.0f;
float fa = fill.a / 255.0f;
for (size_t i = 0; i < pointCount; ++i) {
size_t next = (i + 1) % pointCount;
Vector2f p1 = combinedTransform.transformPoint(getPoint(i));
Vector2f p2 = combinedTransform.transformPoint(getPoint(next));
// Triangle: center, p1, p2
vertices.push_back(transformedCenter.x);
vertices.push_back(transformedCenter.y);
vertices.push_back(p1.x);
vertices.push_back(p1.y);
vertices.push_back(p2.x);
vertices.push_back(p2.y);
// Colors for each vertex
for (int v = 0; v < 3; ++v) {
colors.push_back(fr);
colors.push_back(fg);
colors.push_back(fb);
colors.push_back(fa);
}
}
// Draw fill
if (fill.a > 0 && !vertices.empty()) {
SDL2Renderer::getInstance().drawTriangles(
vertices.data(), vertices.size() / 2,
colors.data(), nullptr, 0
);
}
// Draw outline if thickness > 0
float outlineThickness = getOutlineThickness();
if (outlineThickness > 0) {
Color outline = getOutlineColor();
if (outline.a > 0) {
float or_ = outline.r / 255.0f;
float og = outline.g / 255.0f;
float ob = outline.b / 255.0f;
float oa = outline.a / 255.0f;
// Build outline as quads (two triangles per edge)
vertices.clear();
colors.clear();
for (size_t i = 0; i < pointCount; ++i) {
size_t next = (i + 1) % pointCount;
Vector2f p1 = combinedTransform.transformPoint(getPoint(i));
Vector2f p2 = combinedTransform.transformPoint(getPoint(next));
// Calculate normal direction
Vector2f dir(p2.x - p1.x, p2.y - p1.y);
float len = std::sqrt(dir.x * dir.x + dir.y * dir.y);
if (len > 0) {
dir.x /= len;
dir.y /= len;
}
Vector2f normal(-dir.y * outlineThickness, dir.x * outlineThickness);
// Outer points
Vector2f p1o(p1.x + normal.x, p1.y + normal.y);
Vector2f p2o(p2.x + normal.x, p2.y + normal.y);
// Two triangles for quad
// Triangle 1: p1, p2, p1o
vertices.push_back(p1.x); vertices.push_back(p1.y);
vertices.push_back(p2.x); vertices.push_back(p2.y);
vertices.push_back(p1o.x); vertices.push_back(p1o.y);
// Triangle 2: p2, p2o, p1o
vertices.push_back(p2.x); vertices.push_back(p2.y);
vertices.push_back(p2o.x); vertices.push_back(p2o.y);
vertices.push_back(p1o.x); vertices.push_back(p1o.y);
for (int v = 0; v < 6; ++v) {
colors.push_back(or_);
colors.push_back(og);
colors.push_back(ob);
colors.push_back(oa);
}
}
if (!vertices.empty()) {
SDL2Renderer::getInstance().drawTriangles(
vertices.data(), vertices.size() / 2,
colors.data(), nullptr, 0
);
}
}
}
} }
void VertexArray::draw(RenderTarget& target, RenderStates states) const { void VertexArray::draw(RenderTarget& target, RenderStates states) const {

View file

@ -216,29 +216,101 @@ public:
// For now, stub implementation matching headless // For now, stub implementation matching headless
class Transform { class Transform {
// 3x3 matrix stored as 4x4 for OpenGL compatibility // 3x3 matrix stored as column-major for OpenGL
// [ m00 m01 m02 ] [ m[0] m[4] m[12] ] // [ a c tx ] [ m[0] m[3] m[6] ]
// [ m10 m11 m12 ] -> [ m[1] m[5] m[13] ] // [ b d ty ] -> [ m[1] m[4] m[7] ]
// [ 0 0 1 ] [ 0 0 1 ] // [ 0 0 1 ] [ m[2] m[5] m[8] ]
float m[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1}; float m[9] = {1,0,0, 0,1,0, 0,0,1};
public: public:
Transform() = default; Transform() = default;
Transform& translate(float x, float y) { return *this; } // TODO: Implement
Transform& translate(float x, float y) {
// Combine with translation matrix
m[6] += m[0] * x + m[3] * y;
m[7] += m[1] * x + m[4] * y;
return *this;
}
Transform& translate(const Vector2f& offset) { return translate(offset.x, offset.y); } Transform& translate(const Vector2f& offset) { return translate(offset.x, offset.y); }
Transform& rotate(float angle) { return *this; } // TODO: Implement
Transform& rotate(float angle, const Vector2f& center) { return *this; } // TODO: Implement Transform& rotate(float angle) {
Transform& scale(float factorX, float factorY) { return *this; } // TODO: Implement float rad = angle * 3.14159265f / 180.0f;
float cos_a = std::cos(rad);
float sin_a = std::sin(rad);
float new_m0 = m[0] * cos_a + m[3] * sin_a;
float new_m1 = m[1] * cos_a + m[4] * sin_a;
float new_m3 = m[0] * -sin_a + m[3] * cos_a;
float new_m4 = m[1] * -sin_a + m[4] * cos_a;
m[0] = new_m0; m[1] = new_m1;
m[3] = new_m3; m[4] = new_m4;
return *this;
}
Transform& rotate(float angle, const Vector2f& center) {
translate(center.x, center.y);
rotate(angle);
translate(-center.x, -center.y);
return *this;
}
Transform& scale(float factorX, float factorY) {
m[0] *= factorX; m[1] *= factorX;
m[3] *= factorY; m[4] *= factorY;
return *this;
}
Transform& scale(const Vector2f& factors) { return scale(factors.x, factors.y); } Transform& scale(const Vector2f& factors) { return scale(factors.x, factors.y); }
Vector2f transformPoint(float x, float y) const { return Vector2f(x, y); } // TODO: Implement Vector2f transformPoint(float x, float y) const {
Vector2f transformPoint(const Vector2f& point) const { return point; } return Vector2f(m[0] * x + m[3] * y + m[6],
FloatRect transformRect(const FloatRect& rect) const { return rect; } // TODO: Implement m[1] * x + m[4] * y + m[7]);
}
Vector2f transformPoint(const Vector2f& point) const {
return transformPoint(point.x, point.y);
}
Transform getInverse() const { return Transform(); } // TODO: Implement FloatRect transformRect(const FloatRect& rect) const {
// Transform all four corners and compute bounding box
Vector2f p1 = transformPoint(rect.left, rect.top);
Vector2f p2 = transformPoint(rect.left + rect.width, rect.top);
Vector2f p3 = transformPoint(rect.left, rect.top + rect.height);
Vector2f p4 = transformPoint(rect.left + rect.width, rect.top + rect.height);
Transform operator*(const Transform& rhs) const { return Transform(); } // TODO: Implement float minX = std::min({p1.x, p2.x, p3.x, p4.x});
Vector2f operator*(const Vector2f& point) const { return point; } float maxX = std::max({p1.x, p2.x, p3.x, p4.x});
float minY = std::min({p1.y, p2.y, p3.y, p4.y});
float maxY = std::max({p1.y, p2.y, p3.y, p4.y});
return FloatRect(minX, minY, maxX - minX, maxY - minY);
}
Transform getInverse() const {
// Compute inverse of 3x3 affine matrix
float det = m[0] * m[4] - m[1] * m[3];
if (std::abs(det) < 1e-7f) return Transform();
float invDet = 1.0f / det;
Transform inv;
inv.m[0] = m[4] * invDet;
inv.m[1] = -m[1] * invDet;
inv.m[3] = -m[3] * invDet;
inv.m[4] = m[0] * invDet;
inv.m[6] = (m[3] * m[7] - m[4] * m[6]) * invDet;
inv.m[7] = (m[1] * m[6] - m[0] * m[7]) * invDet;
return inv;
}
Transform operator*(const Transform& rhs) const {
Transform result;
result.m[0] = m[0] * rhs.m[0] + m[3] * rhs.m[1];
result.m[1] = m[1] * rhs.m[0] + m[4] * rhs.m[1];
result.m[3] = m[0] * rhs.m[3] + m[3] * rhs.m[4];
result.m[4] = m[1] * rhs.m[3] + m[4] * rhs.m[4];
result.m[6] = m[0] * rhs.m[6] + m[3] * rhs.m[7] + m[6];
result.m[7] = m[1] * rhs.m[6] + m[4] * rhs.m[7] + m[7];
return result;
}
Vector2f operator*(const Vector2f& point) const { return transformPoint(point); }
static const Transform Identity; static const Transform Identity;
@ -331,10 +403,14 @@ class Shader;
class RenderStates { class RenderStates {
public: public:
Transform transform;
BlendMode blendMode;
const Shader* shader = nullptr;
RenderStates() = default; RenderStates() = default;
RenderStates(const Transform& transform) {} // Implicit conversion from Transform RenderStates(const Transform& t) : transform(t) {}
RenderStates(const BlendMode& mode) {} RenderStates(const BlendMode& mode) : blendMode(mode) {}
RenderStates(const Shader* shader) {} // Implicit conversion from Shader pointer RenderStates(const Shader* s) : shader(s) {}
static const RenderStates Default; static const RenderStates Default;
}; };
@ -386,8 +462,18 @@ public:
void scale(float factorX, float factorY) { scale_.x *= factorX; scale_.y *= factorY; } void scale(float factorX, float factorY) { scale_.x *= factorX; scale_.y *= factorY; }
void scale(const Vector2f& factor) { scale_.x *= factor.x; scale_.y *= factor.y; } void scale(const Vector2f& factor) { scale_.x *= factor.x; scale_.y *= factor.y; }
Transform getTransform() const { return Transform::Identity; } // TODO: Implement Transform getTransform() const {
Transform getInverseTransform() const { return Transform::Identity; } // TODO: Implement Transform transform;
// Apply transformations: translate to position, rotate, scale, translate by -origin
transform.translate(position_.x, position_.y);
transform.rotate(rotation_);
transform.scale(scale_.x, scale_.y);
transform.translate(-origin_.x, -origin_.y);
return transform;
}
Transform getInverseTransform() const {
return getTransform().getInverse();
}
}; };
// ============================================================================= // =============================================================================
@ -411,6 +497,10 @@ public:
virtual FloatRect getLocalBounds() const { return FloatRect(); } virtual FloatRect getLocalBounds() const { return FloatRect(); }
virtual FloatRect getGlobalBounds() const { return FloatRect(); } virtual FloatRect getGlobalBounds() const { return FloatRect(); }
// Virtual methods for shape points (implemented by derived classes)
virtual size_t getPointCount() const = 0;
virtual Vector2f getPoint(size_t index) const = 0;
protected: protected:
void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp void draw(RenderTarget& target, RenderStates states) const override; // Implemented in SDL2Renderer.cpp
}; };
@ -423,6 +513,17 @@ public:
const Vector2f& getSize() const { return size_; } const Vector2f& getSize() const { return size_; }
FloatRect getLocalBounds() const override { return FloatRect(0, 0, size_.x, size_.y); } FloatRect getLocalBounds() const override { return FloatRect(0, 0, size_.x, size_.y); }
FloatRect getGlobalBounds() const override { return FloatRect(position_.x, position_.y, size_.x, size_.y); } FloatRect getGlobalBounds() const override { return FloatRect(position_.x, position_.y, size_.x, size_.y); }
size_t getPointCount() const override { return 4; }
Vector2f getPoint(size_t index) const override {
switch (index) {
case 0: return Vector2f(0, 0);
case 1: return Vector2f(size_.x, 0);
case 2: return Vector2f(size_.x, size_.y);
case 3: return Vector2f(0, size_.y);
default: return Vector2f();
}
}
}; };
class CircleShape : public Shape { class CircleShape : public Shape {
@ -433,8 +534,13 @@ public:
void setRadius(float radius) { radius_ = radius; } void setRadius(float radius) { radius_ = radius; }
float getRadius() const { return radius_; } float getRadius() const { return radius_; }
void setPointCount(size_t count) { pointCount_ = count; } void setPointCount(size_t count) { pointCount_ = count; }
size_t getPointCount() const { return pointCount_; } size_t getPointCount() const override { return pointCount_; }
FloatRect getLocalBounds() const override { return FloatRect(0, 0, radius_ * 2, radius_ * 2); } FloatRect getLocalBounds() const override { return FloatRect(0, 0, radius_ * 2, radius_ * 2); }
Vector2f getPoint(size_t index) const override {
float angle = static_cast<float>(index) / pointCount_ * 2.0f * 3.14159265f;
return Vector2f(radius_ + radius_ * std::cos(angle), radius_ + radius_ * std::sin(angle));
}
}; };
class ConvexShape : public Shape { class ConvexShape : public Shape {
@ -442,9 +548,9 @@ class ConvexShape : public Shape {
public: public:
ConvexShape(size_t pointCount = 0) : points_(pointCount) {} ConvexShape(size_t pointCount = 0) : points_(pointCount) {}
void setPointCount(size_t count) { points_.resize(count); } void setPointCount(size_t count) { points_.resize(count); }
size_t getPointCount() const { return points_.size(); } size_t getPointCount() const override { return points_.size(); }
void setPoint(size_t index, const Vector2f& point) { if (index < points_.size()) points_[index] = point; } void setPoint(size_t index, const Vector2f& point) { if (index < points_.size()) points_[index] = point; }
Vector2f getPoint(size_t index) const { return index < points_.size() ? points_[index] : Vector2f(); } Vector2f getPoint(size_t index) const override { return index < points_.size() ? points_[index] : Vector2f(); }
}; };
// ============================================================================= // =============================================================================