implement rounded corners default tip

Tue, 17 Jun 2025 20:11:53 +0200

author
Mike Becker <universe@uap-core.de>
date
Tue, 17 Jun 2025 20:11:53 +0200
changeset 158
f650994ec543
parent 157
d6c2f028d8c9

implement rounded corners

for the time being this should be enough to close issue #384
later we add anti-aliasing, glow effects, etc.

shader/rectangle_frag.glsl file | annotate | diff | comparison | revisions
src/2d.c file | annotate | diff | comparison | revisions
src/ascension/2d.h file | annotate | diff | comparison | revisions
src/ascension/constants.h file | annotate | diff | comparison | revisions
src/ascension/datatypes.h file | annotate | diff | comparison | revisions
src/ascension/shader.h file | annotate | diff | comparison | revisions
src/shader.c file | annotate | diff | comparison | revisions
src/sprite.c file | annotate | diff | comparison | revisions
test/snake/snake.c file | annotate | diff | comparison | revisions
--- a/shader/rectangle_frag.glsl	Tue Jun 17 19:00:20 2025 +0200
+++ b/shader/rectangle_frag.glsl	Tue Jun 17 20:11:53 2025 +0200
@@ -6,15 +6,91 @@
 #ifndef FILL
 uniform float thickness;
 #endif
+#ifdef ROUNDED_CORNERS
+uniform float radius;
+#endif
 
 void main(void) {
-#ifdef FILL
+#ifdef ROUNDED_CORNERS
+    // Calculate distances from each corner
+    vec2 corner_distances[4];
+    corner_distances[0] = uvcoord - vec2(radius, radius);                         // top-left
+    corner_distances[1] = uvcoord - vec2(size.x - radius, radius);                // top-right
+    corner_distances[2] = uvcoord - vec2(radius, size.y - radius);                // bottom-left
+    corner_distances[3] = uvcoord - vec2(size.x - radius, size.y - radius);       // bottom-right
+
+    // Check if we're in a corner region
+    bool in_corner_region = false;
+    int corner_idx = -1;
+
+    for (int i = 0; i < 4; i++) {
+        vec2 test_point = corner_distances[i];
+        vec2 corner_test = vec2(0.0);
+
+        if (i == 0 && test_point.x < 0.0 && test_point.y < 0.0) { // top-left
+            corner_test = test_point;
+            corner_idx = i;
+            in_corner_region = true;
+            break;
+        } else if (i == 1 && test_point.x > 0.0 && test_point.y < 0.0) { // top-right
+            corner_test = test_point;
+            corner_idx = i;
+            in_corner_region = true;
+            break;
+        } else if (i == 2 && test_point.x < 0.0 && test_point.y > 0.0) { // bottom-left
+            corner_test = test_point;
+            corner_idx = i;
+            in_corner_region = true;
+            break;
+        } else if (i == 3 && test_point.x > 0.0 && test_point.y > 0.0) { // bottom-right
+            corner_test = test_point;
+            corner_idx = i;
+            in_corner_region = true;
+            break;
+        }
+    }
+
+    #ifdef FILL
+    // For filled rectangle with rounded corners
+    if (in_corner_region) {
+        float dist = length(corner_distances[corner_idx]);
+        if (dist > radius) {
+            discard; // Outside the rounded corner
+        }
+    } else if (uvcoord.x < 0.0 || uvcoord.y < 0.0 || uvcoord.x > size.x || uvcoord.y > size.y) {
+        discard; // Outside the rectangle
+    }
     diffuse = color;
-#else
+    #else  // no FILL
+    // For outlined rectangle with rounded corners
+    if (in_corner_region) {
+        float dist = length(corner_distances[corner_idx]);
+        if (dist > radius) {
+            discard; // Outside the rounded corner
+        } else if (dist < radius - thickness) {
+            discard; // Inside the outline
+        }
+        diffuse = color;
+    } else if (any(lessThan(uvcoord, vec2(thickness))) || any(greaterThan(uvcoord, size - thickness))) {
+        // On a straight edge
+        if (uvcoord.x >= 0.0 && uvcoord.y >= 0.0 && uvcoord.x <= size.x && uvcoord.y <= size.y) {
+            diffuse = color;
+        } else {
+            discard;
+        }
+    } else {
+        discard; // Inside the outline
+    }
+    #endif // FILL
+#else // no ROUNDED_CORNERS
+    #ifdef FILL
+    diffuse = color;
+    #else // no FILL
     if (any(notEqual(1.0-step(thickness, uvcoord)+step(size-thickness, uvcoord), vec2(0.0)))) {
         diffuse = color;
     } else {
         discard;
     }
-#endif
+    #endif // FILL
+#endif // ROUNDED_CORNERS
 }
--- a/src/2d.c	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/2d.c	Tue Jun 17 20:11:53 2025 +0200
@@ -39,14 +39,24 @@
     GLint color;
     GLint size;
     GLint thickness;
+    GLint radius;
 } AscRectangleShader;
 
-static void *asc_rectangle_shader_create(bool fill) {
+#define ASC_RECTANGLE_SHADER_FLAG_FILL  1
+#define ASC_RECTANGLE_SHADER_FLAG_ROUND 2
+
+static AscShaderProgram *asc_rectangle_shader_create(int flags) {
     AscShaderCodes codes;
+    const char * const defines[] = {
+        "",
+        "#define FILL",
+        "#define ROUNDED_CORNERS",
+        "#define FILL\n#define ROUNDED_CORNERS",
+    };
     if (asc_shader_load_code_files((AscShaderCodeInfo){
         .files.vtx = "sprite_vtx.glsl",
         .files.frag = "rectangle_frag.glsl",
-        .defines.frag = fill ? "#define FILL" : NULL,
+        .defines.frag = defines[flags],
     }, &codes)) {
         asc_error("Loading sprite shader failed.");
         return NULL;
@@ -58,32 +68,21 @@
     }
     shader->color = glGetUniformLocation(shader->program.gl_id, "color");
     shader->size = glGetUniformLocation(shader->program.gl_id, "size");
-    if (fill) {
+    if (asc_test_flag(flags, ASC_RECTANGLE_SHADER_FLAG_FILL)) {
         shader->thickness = -1;
     } else {
         shader->thickness = glGetUniformLocation(shader->program.gl_id, "thickness");
     }
+    if (asc_test_flag(flags, ASC_RECTANGLE_SHADER_FLAG_ROUND)) {
+        shader->radius = glGetUniformLocation(shader->program.gl_id, "radius");
+    } else {
+        shader->radius = -1;
+    }
     asc_shader_free_codes(codes);
 
     asc_error_catch_all_gl();
 
-    return shader;
-}
-
-static AscShaderProgram *asc_rectangle_shader_fill_create() {
-    return asc_rectangle_shader_create(true);
-}
-
-static AscShaderProgram *asc_rectangle_shader_draw_create() {
-    return asc_rectangle_shader_create(false);
-}
-
-static const AscRectangleShader *asc_rectangle_shader_draw(void) {
-    return asc_shader_lookup_or_create(ASC_SHADER_RECTANGLE_DRAW, asc_rectangle_shader_draw_create);
-}
-
-static const AscRectangleShader *asc_rectangle_shader_fill(void) {
-    return asc_shader_lookup_or_create(ASC_SHADER_RECTANGLE_FILL, asc_rectangle_shader_fill_create);
+    return (AscShaderProgram*) shader;
 }
 
 static void asc_rectangle_destroy(AscSceneNode *node) {
@@ -99,13 +98,25 @@
 
 static void asc_rectangle_draw(const AscCamera *camera, const AscSceneNode *node) {
     asc_ptr_cast(AscRectangle, rectangle, node);
-    bool filled = asc_test_flag(rectangle->flags, ASC_RECTANGLE_FILLED);
+    const bool filled = asc_test_flag(rectangle->flags, ASC_RECTANGLE_FILLED);
+    const bool round = rectangle->radius > 0;
+
+    // Compute shader flags
+    int shader_flags = 0;
+    if (filled) shader_flags |= ASC_RECTANGLE_SHADER_FLAG_FILL;
+    if (round) shader_flags |= ASC_RECTANGLE_SHADER_FLAG_ROUND;
 
-    // Activate shader
-    // TODO: scene should know which shader we are going to activate s.t. it can pre-sort nodes
-    const AscRectangleShader *shader = filled
-                                         ? asc_rectangle_shader_fill()
-                                         : asc_rectangle_shader_draw();
+    // Compute shader ID
+    const int shader_ids[] = {
+        ASC_SHADER_RECTANGLE_DRAW,
+        ASC_SHADER_RECTANGLE_FILL,
+        ASC_SHADER_RECTANGLE_DRAW_ROUND,
+        ASC_SHADER_RECTANGLE_FILL_ROUND,
+    };
+
+    // Look up and activate shader
+    const AscRectangleShader *shader = asc_shader_lookup_or_create(
+        shader_ids[shader_flags], asc_rectangle_shader_create, shader_flags);
     asc_shader_use(&shader->program, camera);
 
     // Upload uniforms
@@ -119,11 +130,13 @@
         rectangle->color.blue,
         rectangle->color.alpha
     );
-    glUniform2f(shader->size, (float) rectangle->width, (float) rectangle->height);
+    glUniform2f(shader->size, rectangle->width, rectangle->height);
 
     if (!filled) {
-        // TODO: implement thickness
-        glUniform1f(shader->thickness, 1);
+        glUniform1f(shader->thickness, rectangle->thickness);
+    }
+    if (round) {
+        glUniform1f(shader->radius, rectangle->radius);
     }
 
     // Draw mesh
@@ -136,15 +149,17 @@
     if (args.bounds.size.width + args.bounds.size.height > 0) {
         rectangle->node.position.x = (float) args.bounds.pos.x;
         rectangle->node.position.y = (float) args.bounds.pos.y;
-        rectangle->width = args.bounds.size.width;
-        rectangle->height = args.bounds.size.height;
+        rectangle->width = (float) args.bounds.size.width;
+        rectangle->height = (float) args.bounds.size.height;
     } else {
         rectangle->node.position.x = (float) args.x;
         rectangle->node.position.y = (float) args.y;
-        rectangle->width = args.width;
-        rectangle->height = args.height;
+        rectangle->width = (float) args.width;
+        rectangle->height = (float) args.height;
     }
 
+    rectangle->thickness = ASC_NONZERO_OR(1.f, args.thickness);
+    rectangle->radius = (float)args.radius;
     rectangle->color = asc_col_itof(asc_context.ink);
 
     if (args.filled) {
--- a/src/ascension/2d.h	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/ascension/2d.h	Tue Jun 17 20:11:53 2025 +0200
@@ -37,8 +37,10 @@
     AscSceneNode node;
     AscMesh mesh;
     asc_col4f color;
-    unsigned int width;
-    unsigned int height;
+    float width;
+    float height;
+    float radius;
+    float thickness;
     int flags;
 } AscRectangle;
 
@@ -48,6 +50,14 @@
     int y;
     unsigned int width;
     unsigned int height;
+    /**
+     * Corner radius.
+     */
+    unsigned int radius;
+    /**
+     * Border thickness
+     */
+    unsigned int thickness;
     bool filled;
 };
 
--- a/src/ascension/constants.h	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/ascension/constants.h	Tue Jun 17 20:11:53 2025 +0200
@@ -32,12 +32,14 @@
 // Internally used shader IDs.
 // --------------------------------------
 
-#define ASC_SHADER_INTERNAL_ID(id)      (1000000000u+id)
+#define ASC_SHADER_INTERNAL_ID(id)          (1000000000u+id)
 
-#define ASC_SHADER_SPRITE_RECT          ASC_SHADER_INTERNAL_ID(1)
-#define ASC_SHADER_SPRITE_UV            ASC_SHADER_INTERNAL_ID(2)
-#define ASC_SHADER_RECTANGLE_DRAW       ASC_SHADER_INTERNAL_ID(3)
-#define ASC_SHADER_RECTANGLE_FILL       ASC_SHADER_INTERNAL_ID(4)
+#define ASC_SHADER_SPRITE_RECT              ASC_SHADER_INTERNAL_ID(1)
+#define ASC_SHADER_SPRITE_UV                ASC_SHADER_INTERNAL_ID(2)
+#define ASC_SHADER_RECTANGLE_DRAW           ASC_SHADER_INTERNAL_ID(3)
+#define ASC_SHADER_RECTANGLE_FILL           ASC_SHADER_INTERNAL_ID(4)
+#define ASC_SHADER_RECTANGLE_DRAW_ROUND     ASC_SHADER_INTERNAL_ID(5)
+#define ASC_SHADER_RECTANGLE_FILL_ROUND     ASC_SHADER_INTERNAL_ID(6)
 
 
 #endif // ASC_CONSTANTS_H
--- a/src/ascension/datatypes.h	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/ascension/datatypes.h	Tue Jun 17 20:11:53 2025 +0200
@@ -52,7 +52,7 @@
  * @param x the value that shall be tested
  * @return x if nonzero, y otherwise
  */
-#define ASC_NONZERO_OR(y, x)  (x ? x : y)
+#define ASC_NONZERO_OR(y, x)  ((x) != 0 ? x : y)
 
 #define asc_test_flag(reg, flag) ((reg & flag) == flag)
 #define asc_test_flag_masked(reg, mask, flag) ((reg & mask) == flag)
--- a/src/ascension/shader.h	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/ascension/shader.h	Tue Jun 17 20:11:53 2025 +0200
@@ -80,13 +80,13 @@
 } AscShaderProgram;
 
 /**
- * The maximum ID for a user defined shader.
+ * The maximum ID for a user-defined shader.
  *
  * The IDs above this number are reserved for the engine.
  */
 #define ASC_SHADER_ID_USER_MAX  1'000'000'000u
 
-typedef AscShaderProgram*(*asc_shader_create_func)(void);
+typedef AscShaderProgram*(*asc_shader_create_func)(int);
 
 /**
  * Loads shader codes from files.
@@ -143,10 +143,11 @@
  *
  * @param id the custom ID of the shader
  * @param create_func the function that creates the shader
+ * @param create_flags flags passed to create_func
  * @return the shader created by the @c create_func
  * @see asc_shader_lookup()
  */
-const void *asc_shader_register(unsigned int id, asc_shader_create_func create_func);
+const void *asc_shader_register(unsigned int id, asc_shader_create_func create_func, int create_flags);
 
 /**
  * Looks up a shader by ID.
@@ -172,11 +173,12 @@
  *
  * @param id the custom ID of the shader
  * @param create_func the function to create the shader
+ * @param create_flags flags passed to create_func
  * @return the found shader or the newly created shader
  * @see asc_shader_lookup()
  * @see asc_shader_register()
  */
-const void *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func);
+const void *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func, int create_flags);
 
 
 /**
--- a/src/shader.c	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/shader.c	Tue Jun 17 20:11:53 2025 +0200
@@ -207,7 +207,7 @@
     cxFreeDefault(codes.frag);
 }
 
-const void *asc_shader_register(unsigned int id, asc_shader_create_func create_func) {
+const void *asc_shader_register(unsigned int id, asc_shader_create_func create_func, int create_flags) {
     AscGLContext *glctx = asc_active_glctx;
 #ifndef NDEBUG
     {
@@ -219,7 +219,7 @@
         }
     }
 #endif
-    AscShaderProgram *prog = create_func();
+    AscShaderProgram *prog = create_func(create_flags);
     prog->id = id;
     cxListAdd(glctx->shaders, prog);
     return prog;
@@ -233,10 +233,10 @@
     return NULL;
 }
 
-const void *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func) {
+const void *asc_shader_lookup_or_create(unsigned int id, asc_shader_create_func create_func, int create_flags) {
     const AscShaderProgram *prog = asc_shader_lookup(id);
     if (prog == NULL) {
-        return asc_shader_register(id, create_func);
+        return asc_shader_register(id, create_func, create_flags);
     }
     return prog;
 }
--- a/src/sprite.c	Tue Jun 17 19:00:20 2025 +0200
+++ b/src/sprite.c	Tue Jun 17 20:11:53 2025 +0200
@@ -42,7 +42,7 @@
     GLint tex;
 } AscSpriteShader;
 
-static void *asc_sprite_shader_create(bool rect) {
+static AscShaderProgram *asc_sprite_shader_create(int rect) {
     AscShaderCodes codes;
     if (asc_shader_load_code_files((AscShaderCodeInfo){
         .files.vtx = "sprite_vtx.glsl",
@@ -62,23 +62,7 @@
 
     asc_error_catch_all_gl();
 
-    return shader;
-}
-
-static AscShaderProgram *asc_sprite_shader_rect_create() {
-    return asc_sprite_shader_create(true);
-}
-
-static AscShaderProgram *asc_sprite_shader_uv_create() {
-    return asc_sprite_shader_create(false);
-}
-
-static const AscSpriteShader *asc_sprite_shader_rect(void) {
-    return asc_shader_lookup_or_create(ASC_SHADER_SPRITE_RECT, asc_sprite_shader_rect_create);
-}
-
-static const AscSpriteShader *asc_sprite_shader_uv(void) {
-    return asc_shader_lookup_or_create(ASC_SHADER_SPRITE_UV, asc_sprite_shader_uv_create);
+    return (AscShaderProgram*) shader;
 }
 
 static void asc_sprite_destroy(AscSceneNode *node) {
@@ -113,8 +97,8 @@
     // TODO: scene should know which shader we are going to activate s.t. it can pre-sort nodes
     const AscSpriteShader *shader =
             sprite->texture->target == GL_TEXTURE_RECTANGLE
-                ? asc_sprite_shader_rect()
-                : asc_sprite_shader_uv();
+                ? asc_shader_lookup_or_create(ASC_SHADER_SPRITE_RECT, asc_sprite_shader_create, 1)
+                : asc_shader_lookup_or_create(ASC_SHADER_SPRITE_UV, asc_sprite_shader_create, 0);
     asc_shader_use(&shader->program, camera);
 
     // Upload model matrix
--- a/test/snake/snake.c	Tue Jun 17 19:00:20 2025 +0200
+++ b/test/snake/snake.c	Tue Jun 17 20:11:53 2025 +0200
@@ -199,7 +199,8 @@
     // TODO: play around with the test rectangle
     asc_ink_rgb(255, 0, 0);
     asc_scene_add_node(MAIN_SCENE,
-        asc_rectangle(.x = 200, .y = 250, .width = 100, .height = 75)
+        asc_rectangle(.x = 200, .y = 250, .width = 100, .height = 75,
+            .thickness = 4, .radius = 15)
     );
 
     // Main Loop

mercurial