diff --git a/ports/headless.c b/ports/headless.c index 99e5ef9..9cf02c0 100644 --- a/ports/headless.c +++ b/ports/headless.c @@ -116,9 +116,9 @@ struct iui_port_ctx { #define get_blue(c) iui_color_blue(c) #define make_color(r, g, b, a) iui_make_color(r, g, b, a) -/* NOTE: Drawing primitives (fill_rounded_rect, draw_line_impl, fill_circle, - * stroke_circle, draw_arc_impl) have been consolidated into port-sw.h as shared - * inline functions: iui_raster_rounded_rect, iui_raster_line_bresenham, +/* NOTE: Drawing primitives (fill_rounded_rect, draw_line, fill_circle, + * stroke_circle, draw_arc) have been consolidated into port-sw.h as shared + * inline functions: iui_raster_rounded_rect, iui_raster_line, * iui_raster_circle_fill, iui_raster_circle_stroke, iui_raster_arc */ @@ -180,8 +180,7 @@ static void headless_draw_line(float x0, #if HEADLESS_ENABLE_FRAMEBUFFER if (ctx->framebuffer) - iui_raster_line_bresenham(&ctx->raster, x0, y0, x1, y1, width, - srgb_color); + iui_raster_line(&ctx->raster, x0, y0, x1, y1, width, srgb_color); #else (void) x0; (void) y0; @@ -307,11 +306,8 @@ static void headless_path_stroke(float width, uint32_t color, void *user) return; } - for (int i = 0; i < ctx->path.count - 1; i++) { - iui_raster_line_bresenham( - &ctx->raster, ctx->path.points_x[i], ctx->path.points_y[i], - ctx->path.points_x[i + 1], ctx->path.points_y[i + 1], width, color); - } + /* Use SDL2-compatible path stroke with round caps and consistent AA */ + iui_raster_path_stroke(&ctx->raster, &ctx->path, width, color); iui_path_reset(&ctx->path); #else (void) width; diff --git a/ports/port-sw.h b/ports/port-sw.h index 9d29467..3e04ef4 100644 --- a/ports/port-sw.h +++ b/ports/port-sw.h @@ -1,5 +1,5 @@ /* - * ports/port-sw.h - Software Rendering Utilities for Framebuffer-Based Ports + * Software Rendering Utilities for Framebuffer-Based Ports * * Consolidated header providing: * - Color manipulation and alpha blending (ARGB32) @@ -334,105 +334,141 @@ static inline void iui_raster_rounded_rect(iui_raster_ctx_t *r, } } -/* Xiaolin Wu's anti-aliased line algorithm (single pixel width) */ -static inline void iui_raster_line_aa(iui_raster_ctx_t *r, +/* Draw capsule (rounded rectangle / stadium shape) using signed distance field. + * A capsule is a line segment with radius - perfect for thick stroke rendering. + * Uses per-pixel distance calculation with AA at edges. + * + * Optimizations: + * - Squared distance early-out avoids sqrtf for solid core pixels + * - Tighter AA fringe for thin lines (radius <= 0.5) improves crispness + * - Pre-clipped bounding box eliminates redundant per-pixel bounds checks + */ +static inline void iui_raster_capsule(iui_raster_ctx_t *r, float x0, float y0, float x1, float y1, + float radius, uint32_t color) { - float dx = x1 - x0; - float dy = y1 - y0; - - if (fabsf(dx) < 0.001f && fabsf(dy) < 0.001f) { - iui_raster_pixel_aa(r, (int) roundf(x0), (int) roundf(y0), color, 1.0f); + if (radius <= 0.0f) return; - } - int steep = fabsf(dy) > fabsf(dx); - - if (steep) { - float t; - t = x0; - x0 = y0; - y0 = t; - t = x1; - x1 = y1; - y1 = t; - t = dx; - dx = dy; - dy = t; - } - - if (x0 > x1) { - float t; - t = x0; - x0 = x1; - x1 = t; - t = y0; - y0 = y1; - y1 = t; - } - - dx = x1 - x0; - float gradient = (fabsf(dx) < 0.001f) ? 0.0f : dy / dx; - - /* First endpoint */ - float xend = roundf(x0); - float yend = y0 + gradient * (xend - x0); - float xgap = 1.0f - (x0 + 0.5f - floorf(x0 + 0.5f)); - int xpxl1 = (int) xend; - int ypxl1 = (int) floorf(yend); - float fpart1 = yend - floorf(yend); - - if (steep) { - iui_raster_pixel_aa(r, ypxl1, xpxl1, color, (1.0f - fpart1) * xgap); - iui_raster_pixel_aa(r, ypxl1 + 1, xpxl1, color, fpart1 * xgap); - } else { - iui_raster_pixel_aa(r, xpxl1, ypxl1, color, (1.0f - fpart1) * xgap); - iui_raster_pixel_aa(r, xpxl1, ypxl1 + 1, color, fpart1 * xgap); - } - - float intery = yend + gradient; + /* Adaptive AA fringe: tighter for thin lines to improve crispness. + * Smoothly interpolate between 0.35 (crisp) and 0.5 (SDL2-compatible) + * over the radius range [0.4, 0.6] to avoid sudden width jumps. + */ + float aa_half; + if (radius <= 0.4f) + aa_half = 0.35f; + else if (radius >= 0.6f) + aa_half = 0.5f; + else + aa_half = 0.35f + (radius - 0.4f) * (0.5f - 0.35f) / (0.6f - 0.4f); + + /* Precompute squared thresholds for early-out optimization */ + float inner_r = radius - aa_half; + float outer_r = radius + aa_half; + float inner_r2 = (inner_r > 0.0f) ? inner_r * inner_r : 0.0f; + float outer_r2 = outer_r * outer_r; + float aa_width = 2.0f * aa_half; + + /* Compute bounding box with tighter margin */ + float margin = outer_r + 0.5f; + float min_xf = fminf(x0, x1) - margin; + float max_xf = fmaxf(x0, x1) + margin; + float min_yf = fminf(y0, y1) - margin; + float max_yf = fmaxf(y0, y1) + margin; + + int min_x = (int) floorf(min_xf); + int max_x = (int) ceilf(max_xf); + int min_y = (int) floorf(min_yf); + int max_y = (int) ceilf(max_yf); + + /* Clip to framebuffer - after this, no per-pixel bounds check needed */ + if (min_x < r->clip_min_x) + min_x = r->clip_min_x; + if (max_x > r->clip_max_x) + max_x = r->clip_max_x; + if (min_y < r->clip_min_y) + min_y = r->clip_min_y; + if (max_y > r->clip_max_y) + max_y = r->clip_max_y; + + if (min_x >= max_x || min_y >= max_y) + return; - /* Second endpoint */ - xend = roundf(x1); - yend = y1 + gradient * (xend - x1); - xgap = x1 + 0.5f - floorf(x1 + 0.5f); - int xpxl2 = (int) xend; - int ypxl2 = (int) floorf(yend); - float fpart2 = yend - floorf(yend); + float dx = x1 - x0; + float dy = y1 - y0; + float len2 = dx * dx + dy * dy; + /* Threshold 1e-6 matches iui_raster_path_stroke's degenerate check */ + float inv_len2 = (len2 > 0.000001f) ? 1.0f / len2 : 0.0f; + + /* Precompute scaled direction for incremental dot product */ + float dx_scaled = dx * inv_len2; + float dy_scaled = dy * inv_len2; + float fx_start = (float) min_x + 0.5f; + float fx_x0 = fx_start - x0; + + uint32_t *row_base = r->framebuffer + (size_t) min_y * (size_t) r->width; + + /* For each pixel, compute distance to line segment */ + for (int py = min_y; py < max_y; py++) { + float fy = (float) py + 0.5f; + float fy_y0 = fy - y0; + + /* Incremental dot product: start value for this row */ + float dot_base = fx_x0 * dx_scaled + fy_y0 * dy_scaled; + float fx = fx_start; + + for (int px = min_x; px < max_x; px++) { + /* Project point onto line segment, clamp t to [0,1] */ + float t; + if (inv_len2 == 0.0f) { + t = 0.0f; + } else { + t = dot_base; + if (t < 0.0f) + t = 0.0f; + else if (t > 1.0f) + t = 1.0f; + } - if (steep) { - iui_raster_pixel_aa(r, ypxl2, xpxl2, color, (1.0f - fpart2) * xgap); - iui_raster_pixel_aa(r, ypxl2 + 1, xpxl2, color, fpart2 * xgap); - } else { - iui_raster_pixel_aa(r, xpxl2, ypxl2, color, (1.0f - fpart2) * xgap); - iui_raster_pixel_aa(r, xpxl2, ypxl2 + 1, color, fpart2 * xgap); - } + /* Closest point on segment */ + float cx = x0 + t * dx; + float cy = y0 + t * dy; + + /* Squared distance from pixel center to closest point */ + float dist_x = fx - cx; + float dist_y = fy - cy; + float dist2 = dist_x * dist_x + dist_y * dist_y; + + /* Early-out using squared distance comparisons (avoids sqrtf) */ + if (dist2 < inner_r2) { + /* Fully inside solid core - direct write, no bounds check */ + row_base[px] = iui_blend_pixel(row_base[px], color); + r->pixels_drawn++; + } else if (dist2 < outer_r2) { + /* In AA band - need sqrtf for accurate coverage */ + float dist = sqrtf(dist2); + float coverage = (outer_r - dist) / aa_width; + row_base[px] = iui_blend_aa(row_base[px], color, coverage); + r->pixels_drawn++; + } + /* else: outside capsule, skip */ - /* Main loop */ - if (steep) { - for (int px = xpxl1 + 1; px < xpxl2; px++) { - int iy = (int) floorf(intery); - float fpart = intery - (float) iy; - iui_raster_pixel_aa(r, iy, px, color, 1.0f - fpart); - iui_raster_pixel_aa(r, iy + 1, px, color, fpart); - intery += gradient; - } - } else { - for (int px = xpxl1 + 1; px < xpxl2; px++) { - int iy = (int) floorf(intery); - float fpart = intery - (float) iy; - iui_raster_pixel_aa(r, px, iy, color, 1.0f - fpart); - iui_raster_pixel_aa(r, px, iy + 1, color, fpart); - intery += gradient; + /* Increment for next pixel */ + dot_base += dx_scaled; + fx += 1.0f; } + row_base += r->width; } } -/* Draw line with thickness using parallel anti-aliased lines */ +/* Draw line with thickness using capsule SDF. + * Uses the same rendering approach as path_stroke for consistency. + * Minimum stroke width is enforced at 1.0px to match SDL2 behavior. + */ static inline void iui_raster_line(iui_raster_ctx_t *r, float x0, float y0, @@ -441,82 +477,12 @@ static inline void iui_raster_line(iui_raster_ctx_t *r, float width, uint32_t color) { - /* Single AA line for thin strokes */ - if (width <= 1.0f) { - iui_raster_line_aa(r, x0, y0, x1, y1, color); - return; - } - - float dx = x1 - x0; - float dy = y1 - y0; - float len = sqrtf(dx * dx + dy * dy); - - if (len < 0.001f) { - iui_raster_pixel(r, (int) x0, (int) y0, color); - return; - } - - /* Perpendicular unit vector */ - float px = -dy / len; - float py = dx / len; - float half_w = width / 2.0f; - - int num_lines = (int) (width + 0.5f); - if (num_lines < 2) - num_lines = 2; - - for (int i = 0; i < num_lines; i++) { - float offset = -half_w + (width * i) / (num_lines - 1); - float ox = px * offset; - float oy = py * offset; - iui_raster_line_aa(r, x0 + ox, y0 + oy, x1 + ox, y1 + oy, color); - } -} - -/* Bresenham line algorithm with thickness (no anti-aliasing, faster) */ -static inline void iui_raster_line_bresenham(iui_raster_ctx_t *r, - float fx0, - float fy0, - float fx1, - float fy1, - float width, - uint32_t color) -{ - int x0 = (int) roundf(fx0); - int y0 = (int) roundf(fy0); - int x1 = (int) roundf(fx1); - int y1 = (int) roundf(fy1); - - int dx = abs(x1 - x0); - int dy = abs(y1 - y0); - int sx = x0 < x1 ? 1 : -1; - int sy = y0 < y1 ? 1 : -1; - int err = dx - dy; - - int thickness = (int) (width + 0.5f); - if (thickness < 1) - thickness = 1; - int half_thick = thickness / 2; - - while (1) { - for (int ty = -half_thick; ty <= half_thick; ty++) { - for (int tx = -half_thick; tx <= half_thick; tx++) - iui_raster_pixel(r, x0 + tx, y0 + ty, color); - } + /* Enforce minimum stroke width like SDL2 and path_stroke */ + if (width < 1.0f) + width = 1.0f; - if (x0 == x1 && y0 == y1) - break; - - int e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; - } - } + float radius = width * 0.5f; + iui_raster_capsule(r, x0, y0, x1, y1, radius, color); } /* Fill circle with anti-aliased edges */ @@ -566,7 +532,10 @@ static inline void iui_raster_circle_fill(iui_raster_ctx_t *r, } } -/* Stroke circle outline using line segments */ +/* Stroke circle outline using SDF (signed distance field) for perfect AA. + * Renders an annulus (ring) by computing distance to the circle center + * and checking if it falls within the stroke band. + */ static inline void iui_raster_circle_stroke(iui_raster_ctx_t *r, float cx, float cy, @@ -574,24 +543,86 @@ static inline void iui_raster_circle_stroke(iui_raster_ctx_t *r, float width, uint32_t color) { - int segments = IUI_CIRCLE_SEGMENTS(radius); - float angle_step = (float) IUI_PORT_PI * 2.f / (float) segments; + if (radius <= 0.0f || width <= 0.0f) + return; - float prev_x = cx + radius; - float prev_y = cy; + float half_w = width * 0.5f; + if (half_w < 0.4f) + half_w = 0.4f; + + float outer_r = radius + half_w + 1.0f; + + /* Bounding box */ + int min_x = (int) floorf(cx - outer_r); + int max_x = (int) ceilf(cx + outer_r); + int min_y = (int) floorf(cy - outer_r); + int max_y = (int) ceilf(cy + outer_r); + + /* Clip to framebuffer */ + if (min_x < r->clip_min_x) + min_x = r->clip_min_x; + if (max_x > r->clip_max_x) + max_x = r->clip_max_x; + if (min_y < r->clip_min_y) + min_y = r->clip_min_y; + if (max_y > r->clip_max_y) + max_y = r->clip_max_y; + + for (int py = min_y; py < max_y; py++) { + float fy = (float) py + 0.5f - cy; + float fy2 = fy * fy; + + for (int px = min_x; px < max_x; px++) { + float fx = (float) px + 0.5f - cx; + + /* Distance from pixel center to circle center */ + float dist_to_center = sqrtf(fx * fx + fy2); + + /* Distance to the ring (annulus) - how far from radius line */ + float dist_to_ring = fabsf(dist_to_center - radius); + + /* AA zone is 1 pixel wide centered on stroke boundary */ + if (dist_to_ring < half_w - 0.5f) { + /* Fully inside stroke */ + iui_raster_pixel(r, px, py, color); + } else if (dist_to_ring < half_w + 0.5f) { + /* AA edge */ + float coverage = (half_w + 0.5f) - dist_to_ring; + iui_raster_pixel_aa(r, px, py, color, coverage); + } + } + } +} - for (int i = 1; i <= segments; i++) { - float angle = angle_step * (float) i; - float curr_x = cx + cosf(angle) * radius; - float curr_y = cy + sinf(angle) * radius; +/* Normalize angle to [0, 2*PI) range */ +static inline float iui_normalize_angle(float angle) +{ + const float two_pi = (float) IUI_PORT_PI * 2.0f; + while (angle < 0.0f) + angle += two_pi; + while (angle >= two_pi) + angle -= two_pi; + return angle; +} - iui_raster_line(r, prev_x, prev_y, curr_x, curr_y, width, color); - prev_x = curr_x; - prev_y = curr_y; +/* Check if angle is within arc range (handles wraparound) */ +static inline int iui_angle_in_arc(float angle, float start, float end) +{ + angle = iui_normalize_angle(angle); + start = iui_normalize_angle(start); + end = iui_normalize_angle(end); + + if (start <= end) { + return angle >= start && angle <= end; + } else { + /* Arc crosses 0/2PI boundary */ + return angle >= start || angle <= end; } } -/* Draw arc using line segments */ +/* Draw arc using SDF for perfect AA. + * Combines radial distance check with angular bounds check. + */ static inline void iui_raster_arc(iui_raster_ctx_t *r, float cx, float cy, @@ -601,24 +632,82 @@ static inline void iui_raster_arc(iui_raster_ctx_t *r, float width, uint32_t color) { - float arc_angle = end_angle - start_angle; - if (arc_angle < 0) - arc_angle += (float) IUI_PORT_PI * 2.f; - - int segments = IUI_ARC_SEGMENTS(radius, arc_angle); - float angle_step = arc_angle / (float) segments; - - float prev_x = cx + cosf(start_angle) * radius; - float prev_y = cy + sinf(start_angle) * radius; + if (radius <= 0.0f || width <= 0.0f) + return; - for (int i = 1; i <= segments; i++) { - float angle = start_angle + angle_step * (float) i; - float curr_x = cx + cosf(angle) * radius; - float curr_y = cy + sinf(angle) * radius; + float half_w = width * 0.5f; + if (half_w < 0.4f) + half_w = 0.4f; + + float outer_r = radius + half_w + 1.0f; + + /* Bounding box */ + int min_x = (int) floorf(cx - outer_r); + int max_x = (int) ceilf(cx + outer_r); + int min_y = (int) floorf(cy - outer_r); + int max_y = (int) ceilf(cy + outer_r); + + /* Clip to framebuffer */ + if (min_x < r->clip_min_x) + min_x = r->clip_min_x; + if (max_x > r->clip_max_x) + max_x = r->clip_max_x; + if (min_y < r->clip_min_y) + min_y = r->clip_min_y; + if (max_y > r->clip_max_y) + max_y = r->clip_max_y; + + /* Precompute arc endpoint positions for cap rendering */ + float start_x = cx + cosf(start_angle) * radius; + float start_y = cy + sinf(start_angle) * radius; + float end_x = cx + cosf(end_angle) * radius; + float end_y = cy + sinf(end_angle) * radius; + + for (int py = min_y; py < max_y; py++) { + float fy = (float) py + 0.5f - cy; + float fy2 = fy * fy; + + for (int px = min_x; px < max_x; px++) { + float fx = (float) px + 0.5f - cx; + float dist_to_center = sqrtf(fx * fx + fy2); + + /* Skip if too far from the arc radius */ + if (dist_to_center < radius - half_w - 1.0f || + dist_to_center > radius + half_w + 1.0f) + continue; + + /* Calculate angle of this pixel relative to center */ + float pixel_angle = atan2f(fy, fx); + + /* Check if within arc angular range */ + int in_arc = iui_angle_in_arc(pixel_angle, start_angle, end_angle); + + float dist; + if (in_arc) { + /* Inside arc angular range - use radial distance */ + dist = fabsf(dist_to_center - radius); + } else { + /* Outside arc - compute distance to nearest endpoint (cap) */ + float dx_start = (float) px + 0.5f - start_x; + float dy_start = (float) py + 0.5f - start_y; + float dist_start = + sqrtf(dx_start * dx_start + dy_start * dy_start); + + float dx_end = (float) px + 0.5f - end_x; + float dy_end = (float) py + 0.5f - end_y; + float dist_end = sqrtf(dx_end * dx_end + dy_end * dy_end); + + dist = fminf(dist_start, dist_end); + } - iui_raster_line(r, prev_x, prev_y, curr_x, curr_y, width, color); - prev_x = curr_x; - prev_y = curr_y; + /* AA zone is 1 pixel wide centered on stroke boundary */ + if (dist < half_w - 0.5f) { + iui_raster_pixel(r, px, py, color); + } else if (dist < half_w + 0.5f) { + float coverage = (half_w + 0.5f) - dist; + iui_raster_pixel_aa(r, px, py, color, coverage); + } + } } } @@ -788,6 +877,44 @@ static inline void iui_path_curve_to_scaled(iui_path_state_t *p, p->pen_y = p3y; } +/* Stroke path with round caps - matches SDL2's geometry-based rendering. + * Key behaviors to match SDL2: + * - Minimum stroke width of 1.0px + * - Consistent 0.5px AA fringe + * - Round caps at path endpoints + * - Uses capsule SDF for all segments (consistent AA regardless of angle) + */ +static inline void iui_raster_path_stroke(iui_raster_ctx_t *r, + iui_path_state_t *p, + float width, + uint32_t color) +{ + if (p->count < 2) + return; + + /* Enforce minimum stroke width like SDL2 */ + if (width < 1.0f) + width = 1.0f; + + float radius = width * 0.5f; + + /* Draw all segments using capsule SDF for consistent AA. + * Capsule geometry inherently provides round caps at endpoints, + * so no explicit cap drawing is needed. + */ + for (int i = 0; i < p->count - 1; i++) { + float x0 = p->points_x[i], y0 = p->points_y[i]; + float x1 = p->points_x[i + 1], y1 = p->points_y[i + 1]; + + /* Skip degenerate segments (threshold matches iui_raster_capsule) */ + float dx = x1 - x0, dy = y1 - y0; + if (dx * dx + dy * dy < 0.001f * 0.001f) + continue; + + iui_raster_capsule(r, x0, y0, x1, y1, radius, color); + } +} + #ifdef __cplusplus } #endif diff --git a/ports/wasm.c b/ports/wasm.c index 054a5b8..b8aa435 100644 --- a/ports/wasm.c +++ b/ports/wasm.c @@ -171,12 +171,8 @@ static void wasm_path_stroke(float width, uint32_t color, void *user) return; } - /* Draw path segments using rasterizer */ - for (int i = 0; i < ctx->path.count - 1; i++) { - iui_raster_line(&ctx->raster, ctx->path.points_x[i], - ctx->path.points_y[i], ctx->path.points_x[i + 1], - ctx->path.points_y[i + 1], width, color); - } + /* Use SDL2-compatible path stroke with round caps and consistent AA */ + iui_raster_path_stroke(&ctx->raster, &ctx->path, width, color); iui_path_reset(&ctx->path); }