Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 188 additions & 33 deletions Sources/ObjectivelyMVC/Renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
*/

#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>

Expand Down Expand Up @@ -158,6 +159,87 @@ static SDL_FRect textureRegion(const Texture *texture, const SDL_Rect *src) {
return (SDL_FRect) { src->x / w, src->y / h, src->w / w, src->h / h };
}

/**
* @brief The color at `x, y` for a gradient of `angle` degrees across `bounds`.
* @details `angle` is measured clockwise from north, so `0` fills upwards and `90` to the
* right. The interpolant is the point's projection onto the gradient axis, normalized by the
* extent of `bounds` along that axis, which makes the color an affine function of position:
* barycentric interpolation across the triangle then reproduces the gradient exactly, at any
* angle, from colors assigned only at the vertices.
*/
static SDL_Color gradientColor(float x, float y, const SDL_FRect *bounds, int angle,
const SDL_Color *from, const SDL_Color *to) {

const float radians = float_radians((float) angle);
const float dx = sinf(radians), dy = -cosf(radians);

const float cx = bounds->x + bounds->w * 0.5f;
const float cy = bounds->y + bounds->h * 0.5f;

// the extent of the bounds along the axis, which is the box's support in that direction
const float extent = fabsf(bounds->w * dx) + fabsf(bounds->h * dy);

float t = 0.5f;
if (extent > 0.001f) {
t = 0.5f + ((x - cx) * dx + (y - cy) * dy) / extent;
t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
}

return (SDL_Color) {
(Uint8) (from->r + (to->r - from->r) * t + 0.5f),
(Uint8) (from->g + (to->g - from->g) * t + 0.5f),
(Uint8) (from->b + (to->b - from->b) * t + 0.5f),
(Uint8) (from->a + (to->a - from->a) * t + 0.5f),
};
}

/**
* @brief Appends the quads stroking `points`, of `width`, optionally closing the loop.
* @remarks Each segment is stroked as its own quad, centered on the segment. The joints are
* therefore not mitered; at the one and two pixel widths borders actually use, the notch is
* smaller than a pixel.
*/
static void strokePolyline(const Renderer *self, const SDL_Point *points, size_t count,
bool closed, float width, const SDL_Color *color) {

const size_t segments = closed ? count : count - 1;

MVC_Vertex verts[16 * 6];
const size_t batchSize = lengthof(verts) / 6;

for (size_t s = 0; s < segments; ) {

const size_t batch = min(segments - s, batchSize);

for (size_t i = 0; i < batch; i++, s++) {

const SDL_Point *a = &points[s], *b = &points[(s + 1) % count];

const float ax = (float) a->x, ay = (float) a->y;
const float bx = (float) b->x, by = (float) b->y;

const float dx = bx - ax, dy = by - ay;
const float len = sqrtf(dx * dx + dy * dy);

float nx = 0.0f, ny = 0.0f;
if (len > 0.001f) {
nx = (-dy / len) * width * 0.5f;
ny = ( dx / len) * width * 0.5f;
}

MVC_Vertex *v = &verts[i * 6];
v[0] = (MVC_Vertex) { { { ax - nx, ay - ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[1] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[2] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[3] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[4] = (MVC_Vertex) { { { bx + nx, by + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[5] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } };
}

$(self, pushDrawArrays, verts, batch * 6, NULL, color);
}
}

/**
* @fn void Renderer::drawBevel(const Renderer *self, const SDL_Rect *rect, int radius, int width, const SDL_Color *topLeft, const SDL_Color *bottomRight)
* @memberof Renderer
Expand Down Expand Up @@ -201,39 +283,78 @@ static void drawLines(const Renderer *self, const SDL_Point *points, size_t coun
return;
}

const size_t segments = count - 1;
strokePolyline(self, points, count, false, 1.f, color);
}

MVC_Vertex verts[16 * 6];
const size_t batchSize = lengthof(verts) / 6;
/**
* @fn void Renderer::drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width, const SDL_Color *color)
* @memberof Renderer
*/
static void drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width,
const SDL_Color *color) {

for (size_t s = 0; s < segments; ) {
assert(points);
assert(color);

const size_t batch = min(segments - s, batchSize);
if (count < 3 || width < 1) {
return;
}

for (size_t i = 0; i < batch; i++, s++) {
strokePolyline(self, points, count, true, (float) width, color);
}

const float ax = (float) points[s].x, ay = (float) points[s].y;
const float bx = (float) points[s + 1].x, by = (float) points[s + 1].y;
/**
* @fn void Renderer::drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count, int angle, const SDL_Color *from, const SDL_Color *to)
* @memberof Renderer
*/
static void drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count,
int angle, const SDL_Color *from, const SDL_Color *to) {

const float dx = bx - ax, dy = by - ay;
const float len = sqrtf(dx * dx + dy * dy);
assert(points);
assert(from);

float nx = 0.0f, ny = 0.0f;
if (len > 0.001f) {
nx = (-dy / len) * 0.5f;
ny = ( dx / len) * 0.5f;
}
if (count < 3) {
return;
}

MVC_Vertex *v = &verts[i * 6];
v[0] = (MVC_Vertex) { { { ax - nx, ay - ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[1] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[2] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[3] = (MVC_Vertex) { { { ax + nx, ay + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[4] = (MVC_Vertex) { { { bx + nx, by + ny } }, { { 0.0f, 0.0f } }, { 0 } };
v[5] = (MVC_Vertex) { { { bx - nx, by - ny } }, { { 0.0f, 0.0f } }, { 0 } };
SDL_FRect bounds = { (float) points[0].x, (float) points[0].y, 0.f, 0.f };
float x2 = bounds.x, y2 = bounds.y;

for (size_t i = 1; i < count; i++) {
bounds.x = min(bounds.x, (float) points[i].x);
bounds.y = min(bounds.y, (float) points[i].y);
x2 = max(x2, (float) points[i].x);
y2 = max(y2, (float) points[i].y);
}

bounds.w = x2 - bounds.x;
bounds.h = y2 - bounds.y;

// a convex polygon triangulates as a fan from its first vertex
const size_t triangles = count - 2;

MVC_Vertex verts[16 * 3];
const size_t batchSize = lengthof(verts) / 3;

for (size_t t = 0; t < triangles; ) {

const size_t batch = min(triangles - t, batchSize);

for (size_t i = 0; i < batch; i++, t++) {

const SDL_Point *p[3] = { &points[0], &points[t + 1], &points[t + 2] };

for (size_t j = 0; j < 3; j++) {

const float x = (float) p[j]->x, y = (float) p[j]->y;

verts[i * 3 + j] = (MVC_Vertex) { { { x, y } }, { { 0.f, 0.f } },
to ? gradientColor(x, y, &bounds, angle, from, to) : *from
};
}
}

$(self, pushDrawArrays, verts, batch * 6, NULL, color);
$(self, pushDrawArrays, verts, batch * 3, NULL, NULL);
}
}

Expand Down Expand Up @@ -313,6 +434,30 @@ static void drawRoundedRectFilled(const Renderer *self, const SDL_Rect *rect, in
$(self, pushDrawArrays, verts, 6, NULL, color);
}

/**
* @fn void Renderer::drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius, int angle, const SDL_Color *from, const SDL_Color *to)
* @memberof Renderer
*/
static void drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius,
int angle, const SDL_Color *from, const SDL_Color *to) {

assert(rect);
assert(from);
assert(to);

SDL_FRect frect;
SDL_RectToFRect(rect, &frect);

MVC_Vertex verts[6];
roundedRectVertices(verts, &frect, radius, 0, NULL);

for (size_t i = 0; i < lengthof(verts); i++) {
verts[i].color = gradientColor(verts[i].position.x, verts[i].position.y, &frect, angle, from, to);
}

$(self, pushDrawArrays, verts, lengthof(verts), NULL, NULL);
}

/**
* @fn void Renderer::drawRoundedTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, int radius, const SDL_Color *color)
* @memberof Renderer
Expand Down Expand Up @@ -461,18 +606,24 @@ static void endFrame(Renderer *self) {
$(self->commands, pushVertexUniformData, 0, projection.f, sizeof(projection));

$(renderPass, bindPipeline, self->pipeline);
$(renderPass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = self->vertexBuffer->buffer }, 1);

for (size_t i = 0; i < self->drawArrays->count; i++) {
const MVC_DrawArrays *draw = VectorElement(self->drawArrays, MVC_DrawArrays, i);
// the vertex buffer is created only once something has been drawn into the frame, so a
// frame that drew nothing has none to bind
if (vertexCount) {

$(renderPass, bindVertexBuffers, 0, &(SDL_GPUBufferBinding) { .buffer = self->vertexBuffer->buffer }, 1);

for (size_t i = 0; i < self->drawArrays->count; i++) {
const MVC_DrawArrays *draw = VectorElement(self->drawArrays, MVC_DrawArrays, i);

$(renderPass, setScissor, &draw->scissor);
$(renderPass, setScissor, &draw->scissor);

$(renderPass, bindFragmentSamplers, 0, &(SDL_GPUTextureSamplerBinding) {
.texture = draw->texture->texture, .sampler = self->sampler->sampler,
}, 1);
$(renderPass, bindFragmentSamplers, 0, &(SDL_GPUTextureSamplerBinding) {
.texture = draw->texture->texture, .sampler = self->sampler->sampler,
}, 1);

$(renderPass, drawPrimitives, draw->vertexCount, 1, draw->firstVertex, 0);
$(renderPass, drawPrimitives, draw->vertexCount, 1, draw->firstVertex, 0);
}
}

release(renderPass);
Expand Down Expand Up @@ -509,7 +660,6 @@ static Renderer *initWithDevice(Renderer *self, RenderDevice *device) {
static void pushDrawArrays(const Renderer *self, const MVC_Vertex *verts, size_t count, Texture *texture, const SDL_Color *color) {

assert(verts);
assert(color);

const MVC_DrawArrays draw = {
.firstVertex = (Uint32) self->vertices->count,
Expand All @@ -527,7 +677,9 @@ static void pushDrawArrays(const Renderer *self, const MVC_Vertex *verts, size_t
MVC_Vertex *out = VectorElement(vertices, MVC_Vertex, vertices->count);
for (size_t i = 0; i < count; i++) {
out[i] = verts[i];
out[i].color = *color;
if (color) {
out[i].color = *color;
}
}

vertices->count += count;
Expand Down Expand Up @@ -709,10 +861,13 @@ static void initialize(Class *clazz) {
((RendererInterface *) clazz->interface)->drawBevel = drawBevel;
((RendererInterface *) clazz->interface)->drawLine = drawLine;
((RendererInterface *) clazz->interface)->drawLines = drawLines;
((RendererInterface *) clazz->interface)->drawPolygon = drawPolygon;
((RendererInterface *) clazz->interface)->drawPolygonFilled = drawPolygonFilled;
((RendererInterface *) clazz->interface)->drawRect = drawRect;
((RendererInterface *) clazz->interface)->drawRectFilled = drawRectFilled;
((RendererInterface *) clazz->interface)->drawRoundedRect = drawRoundedRect;
((RendererInterface *) clazz->interface)->drawRoundedRectFilled = drawRoundedRectFilled;
((RendererInterface *) clazz->interface)->drawRoundedRectGradientFilled = drawRoundedRectGradientFilled;
((RendererInterface *) clazz->interface)->drawRoundedTexture = drawRoundedTexture;
((RendererInterface *) clazz->interface)->drawRoundedTextureRegion = drawRoundedTextureRegion;
((RendererInterface *) clazz->interface)->drawTexture = drawTexture;
Expand Down
52 changes: 51 additions & 1 deletion Sources/ObjectivelyMVC/Renderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,39 @@ struct RendererInterface {
*/
void (*drawLines)(const Renderer *self, const SDL_Point *points, size_t count, const SDL_Color *color);

/**
* @fn void Renderer::drawPolygon(const Renderer *self, const SDL_Point *points, size_t count, int width, const SDL_Color *color)
* @brief Records a closed polygon outline of the given width.
* @details The stroke is centered on each edge. Joints are not mitered, so a width well
* above the one or two pixels a border uses will notch at sharp corners.
* @param self The Renderer.
* @param points The points, in order; the last is joined back to the first.
* @param count The number of points; fewer than three draws nothing.
* @param width The stroke width.
* @param color The outline color.
* @memberof Renderer
*/
void (*drawPolygon)(const Renderer *self, const SDL_Point *points, size_t count, int width,
const SDL_Color *color);

/**
* @fn void Renderer::drawPolygonFilled(const Renderer *self, const SDL_Point *points, size_t count, int angle, const SDL_Color *from, const SDL_Color *to)
* @brief Records a filled convex polygon, optionally with a linear gradient.
* @details The polygon is triangulated as a fan from its first vertex, so it MUST be
* convex. It is drawn without the rounded rectangle shader, and so without its
* anti-aliasing: diagonal edges are hard.
* @param self The Renderer.
* @param points The points, in order.
* @param count The number of points; fewer than three draws nothing.
* @param angle The gradient angle in degrees, clockwise from north. Ignored when `to` is
* `NULL`.
* @param from The fill color, and the near color of the gradient.
* @param to The far color of the gradient, or `NULL` to fill flat with `from`.
* @memberof Renderer
*/
void (*drawPolygonFilled)(const Renderer *self, const SDL_Point *points, size_t count,
int angle, const SDL_Color *from, const SDL_Color *to);

/**
* @fn void Renderer::drawRect(const Renderer *self, const SDL_Rect *rect, const SDL_Color *color)
* @brief Records a rectangle outline.
Expand Down Expand Up @@ -274,6 +307,21 @@ struct RendererInterface {
*/
void (*drawRoundedRectFilled)(const Renderer *self, const SDL_Rect *rect, int radius, const SDL_Color *color);

/**
* @fn void Renderer::drawRoundedRectGradientFilled(const Renderer *self, const SDL_Rect *rect, int radius, int angle, const SDL_Color *from, const SDL_Color *to)
* @brief Records a rounded rectangle filled with a linear gradient.
* @param self The Renderer.
* @param rect The rectangle.
* @param radius The corner radius.
* @param angle The gradient angle in degrees, clockwise from north: `0` fills upwards,
* `90` to the right, `180` downwards.
* @param from The near color of the gradient.
* @param to The far color of the gradient.
* @memberof Renderer
*/
void (*drawRoundedRectGradientFilled)(const Renderer *self, const SDL_Rect *rect, int radius,
int angle, const SDL_Color *from, const SDL_Color *to);

/**
* @fn void Renderer::drawRoundedTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, int radius, const SDL_Color *color)
* @brief Records a textured quad clipped to a rounded rectangle.
Expand Down Expand Up @@ -374,7 +422,9 @@ struct RendererInterface {
* @param verts The vertices to append (in logical screen coordinates).
* @param count The number of vertices.
* @param texture The texture to bind, or `NULL` to use the 1×1 white fallback.
* @param color The color multiplier applied in the fragment shader.
* @param color The color to apply to every vertex, or `NULL` to keep the colors the
* vertices already carry. MVC_Vertex::color is interpolated across the triangle, so
* `NULL` is how a caller draws a gradient.
* @memberof Renderer
*/
void (*pushDrawArrays)(const Renderer *self, const MVC_Vertex *verts, size_t count,
Expand Down
5 changes: 4 additions & 1 deletion Sources/ObjectivelyMVC/StackView.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static void applyStyle(View *self, const Style *style) {

const Inlet inlets[] = MakeInlets(
MakeInlet("axis", InletTypeEnum, &this->axis, (ident) StackViewAxisNames),
MakeInlet("reversed", InletTypeBool, &this->reversed, NULL),
MakeInlet("distribution", InletTypeEnum, &this->distribution, (ident) StackViewDistributionNames),
MakeInlet("spacing", InletTypeInteger, &this->spacing, NULL)
);
Expand Down Expand Up @@ -113,7 +114,9 @@ static void layoutSubviews(View *self) {

const float scale = requestedSize ? availableSize / (float) requestedSize : 1.f;

for (size_t i = 0; i < subviews->count; i++) {
for (size_t j = 0; j < subviews->count; j++) {

const size_t i = this->reversed ? subviews->count - 1 - j : j;

View *subview = $(subviews, objectAtIndex, i);

Expand Down
Loading