#include using namespace metal; #define M_PI_F 3.14159265358979323846f struct VertexOut { float4 position [[position]]; float2 uv; float4 color; }; struct ShapeParams { float4 fill_color; float4 stroke_color; float2 size; float radius; float feather; float stroke_thickness; float stroke_align; int shape_type; // 0=rect (rounded via radius), 1=circle, 2=ellipse, 3=pill int corner_style; float dash_len; float gap_len; float dash_offset; int cap_type; // 0=butt, 1=square-ish, 2=round-ish float4 uv_transform; float uv_rotate; int has_texture; float opacity; float _pad; }; // -------- SDFs -------- float sdf_rounded_rect(float2 p, float2 b, float r) { float2 q = abs(p) - b + r; return min(max(q.x, q.y), 0.0f) + length(max(q, 0.0f)) - r; } float sdf_circle(float2 p, float r) { return length(p) - r; } float sdf_ellipse(float2 p, float2 ab) { float2 pn = p / ab; float d = length(pn) - 1.0f; return d * min(ab.x, ab.y); } float sdf_pill(float2 p, float2 b) { float r = min(b.x, b.y); float2 q = abs(p) - float2(b.x - r, b.y - r); return length(max(q, 0.0f)) + min(max(q.x, q.y), 0.0f) - r; } float compute_sdf(float2 p, float2 half_size, int shape_type, float radius) { if (shape_type == 0) { float r = min(radius, min(half_size.x, half_size.y)); return sdf_rounded_rect(p, half_size, r); } else if (shape_type == 1) { float r = min(half_size.x, half_size.y); return sdf_circle(p, r); } else if (shape_type == 2) { return sdf_ellipse(p, half_size); } else { return sdf_pill(p, half_size); } } // -------- Helpers -------- float wrap_angle_0_2pi(float a) { float w = fmod(a, 2.0f * M_PI_F); if (w < 0.0f) w += 2.0f * M_PI_F; return w; } // Project arbitrary p to ellipse boundary (stable and cheap) // ab are ellipse radii (half_size.x, half_size.y) float2 ellipse_project_boundary(float2 p, float2 ab) { float2 safe_ab = max(ab, float2(1e-6f)); float2 pn = p / safe_ab; float len_pn = length(pn); if (len_pn < 1e-6f) return float2(ab.x, 0.0f); float2 dir = pn / len_pn; return dir * ab; } // Numeric arc length from 0..theta for ellipse with radii a,b using midpoint rule. float ellipse_arclen_0_theta(float a, float b, float theta) { const int N = 12; float t = 0.0f; float dt = theta / float(N); float sum = 0.0f; for (int i = 0; i < N; i++) { float tm = t + 0.5f * dt; float s = sin(tm); float c = cos(tm); float ds = sqrt(a * a * s * s + b * b * c * c); sum += ds * dt; t += dt; } return sum; } float ellipse_perimeter_ramanujan(float a, float b) { float apb = a + b; float h = (a - b) * (a - b) / max(apb * apb, 1e-6f); return M_PI_F * apb * (1.0f + (3.0f * h) / (10.0f + sqrt(max(4.0f - 3.0f * h, 1e-6f)))); } // Rounded-rect boundary projection (for stable dash phase across stroke width) float2 rounded_rect_project_boundary(float2 p, float2 half_size, float r) { float2 inner = max(half_size - r, float2(0.0f)); float2 ap = abs(p); if (ap.x > inner.x && ap.y > inner.y && r > 0.0f) { float sx = (p.x < 0.0f) ? -1.0f : 1.0f; float sy = (p.y < 0.0f) ? -1.0f : 1.0f; float2 c = float2(inner.x * sx, inner.y * sy); float2 v = p - c; float lv = length(v); if (lv < 1e-6f) return c + float2(r, 0.0f); return c + v * (r / lv); } float dx = half_size.x - ap.x; float dy = half_size.y - ap.y; if (dx < dy) { float sx = (p.x < 0.0f) ? -1.0f : 1.0f; return float2(sx * half_size.x, clamp(p.y, -inner.y, inner.y)); } float sy = (p.y < 0.0f) ? -1.0f : 1.0f; return float2(clamp(p.x, -inner.x, inner.x), sy * half_size.y); } // Rounded-rect perimeter distance along boundary, starting at +X mid, CCW. float rounded_rect_dist_along(float2 pb, float2 half_size, float r) { float2 inner = max(half_size - r, float2(0.0f)); float ix = inner.x; float iy = inner.y; float seg_right_up = iy; float seg_arc = 0.5f * M_PI_F * r; float seg_top = 2.0f * ix; float seg_left = 2.0f * iy; float seg_bottom = 2.0f * ix; float ax = abs(pb.x); float ay = abs(pb.y); // Right edge if (pb.x > 0.0f && ay <= iy + 1e-5f) { if (pb.y >= 0.0f) return pb.y; float tail = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom + seg_arc; return tail + (pb.y + iy); } // Top edge if (pb.y > 0.0f && ax <= ix + 1e-5f) { float s0 = seg_right_up + seg_arc; return s0 + (ix - pb.x); } // Left edge if (pb.x < 0.0f && ay <= iy + 1e-5f) { float s0 = seg_right_up + seg_arc + seg_top + seg_arc; return s0 + (iy - pb.y); } // Bottom edge if (pb.y < 0.0f && ax <= ix + 1e-5f) { float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc; return s0 + (pb.x + ix); } // Corners if (pb.x > 0.0f && pb.y > 0.0f) { float2 c = float2(ix, iy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a, 0.0f, 0.5f * M_PI_F); return seg_right_up + r * local; } if (pb.x < 0.0f && pb.y > 0.0f) { float2 c = float2(-ix, iy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(M_PI_F - a, 0.0f, 0.5f * M_PI_F); float s0 = seg_right_up + seg_arc + seg_top; return s0 + r * local; } if (pb.x < 0.0f && pb.y < 0.0f) { float2 c = float2(-ix, -iy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a - M_PI_F, 0.0f, 0.5f * M_PI_F); float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left; return s0 + r * local; } // Bottom-right { float2 c = float2(ix, -iy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = 0.0f; if (a >= 1.5f * M_PI_F) local = clamp(a - 1.5f * M_PI_F, 0.0f, 0.5f * M_PI_F); float s0 = seg_right_up + seg_arc + seg_top + seg_arc + seg_left + seg_arc + seg_bottom; return s0 + r * local; } } // -------- Pill helpers -------- float pill_perimeter(float2 half_size) { float r = min(half_size.x, half_size.y); float major = max(half_size.x, half_size.y); float straight = max(major - r, 0.0f); return 4.0f * straight + 2.0f * M_PI_F * r; } float2 pill_project_boundary(float2 p, float2 half_size) { float r = min(half_size.x, half_size.y); bool horiz = (half_size.x >= half_size.y); if (horiz) { float cx = max(half_size.x - r, 0.0f); float2 c = float2((p.x >= 0.0f ? cx : -cx), 0.0f); float2 v = p - c; float lv = length(v); if (lv < 1e-6f) return c + float2(r, 0.0f); return c + v * (r / lv); } float cy = max(half_size.y - r, 0.0f); float2 c = float2(0.0f, (p.y >= 0.0f ? cy : -cy)); float2 v = p - c; float lv = length(v); if (lv < 1e-6f) return c + float2(r, 0.0f); return c + v * (r / lv); } float pill_dist_along(float2 pb, float2 half_size) { float r = min(half_size.x, half_size.y); bool horiz = (half_size.x >= half_size.y); if (horiz) { float cx = max(half_size.x - r, 0.0f); if (pb.x > cx - 1e-5f) { float2 c = float2(cx, 0.0f); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a, 0.0f, M_PI_F); return r * local; } if (abs(pb.y - r) <= 1e-3f && abs(pb.x) <= cx + 1e-3f) { float s0 = M_PI_F * r; return s0 + (cx - pb.x); } if (pb.x < -cx + 1e-5f) { float2 c = float2(-cx, 0.0f); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a - M_PI_F, 0.0f, M_PI_F); float s0 = M_PI_F * r + 2.0f * cx; return s0 + r * local; } float s0 = M_PI_F * r + 2.0f * cx + M_PI_F * r; return s0 + (pb.x + cx); } float cy = max(half_size.y - r, 0.0f); if (pb.y > cy - 1e-5f) { float2 c = float2(0.0f, cy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a, 0.0f, M_PI_F); return r * local; } if (abs(pb.x + r) <= 1e-3f && abs(pb.y) <= cy + 1e-3f) { float s0 = M_PI_F * r; return s0 + (cy - pb.y); } if (pb.y < -cy + 1e-5f) { float2 c = float2(0.0f, -cy); float2 v = pb - c; float a = wrap_angle_0_2pi(atan2(v.y, v.x)); float local = clamp(a - M_PI_F, 0.0f, M_PI_F); float s0 = M_PI_F * r + 2.0f * cy; return s0 + r * local; } float s0 = M_PI_F * r + 2.0f * cy + M_PI_F * r; return s0 + (pb.y + cy); } // -------- Perimeter mapping for dashes -------- float perimeter_dist_along(float2 p, float2 half_size, int shape_type, float radius) { if (shape_type == 1) { float r = min(half_size.x, half_size.y); float2 pb = (length(p) < 1e-6f) ? float2(r, 0.0f) : normalize(p) * r; float theta = wrap_angle_0_2pi(atan2(pb.y, pb.x)); return r * theta; } if (shape_type == 2) { float a = max(half_size.x, 1e-6f); float b = max(half_size.y, 1e-6f); float2 pb = ellipse_project_boundary(p, float2(a, b)); float theta = wrap_angle_0_2pi(atan2(pb.y / b, pb.x / a)); return ellipse_arclen_0_theta(a, b, theta); } if (shape_type == 3) { float2 pb = pill_project_boundary(p, half_size); return pill_dist_along(pb, half_size); } float r = min(radius, min(half_size.x, half_size.y)); float2 pb = rounded_rect_project_boundary(p, half_size, r); return rounded_rect_dist_along(pb, half_size, r); } float perimeter_total(float2 half_size, int shape_type, float radius) { if (shape_type == 1) { float r = min(half_size.x, half_size.y); return 2.0f * M_PI_F * r; } if (shape_type == 2) { float a = max(half_size.x, 1e-6f); float b = max(half_size.y, 1e-6f); return ellipse_perimeter_ramanujan(a, b); } if (shape_type == 3) { return pill_perimeter(half_size); } float r = min(radius, min(half_size.x, half_size.y)); float2 inner = max(half_size - r, float2(0.0f)); return 4.0f * (inner.x + inner.y) + 2.0f * M_PI_F * r; } float dash_mask_perimeter(float dist_along, float perimeter, float dash_len, float gap_len, float dash_offset, int cap_type, float aa) { if (dash_len <= 0.0f) return 1.0f; float pattern_len = max(dash_len + gap_len, 1e-6f); float s = dist_along + dash_offset; float pwrap = max(perimeter, 1e-6f); float s_wrap = fmod(s, pwrap); if (s_wrap < 0.0f) s_wrap += pwrap; float pos = fmod(s_wrap, pattern_len); if (pos < 0.0f) pos += pattern_len; if (cap_type == 0) { return pos < dash_len ? 1.0f : 0.0f; } if (cap_type == 1) { float edge = dash_len; return 1.0f - smoothstep(edge - aa, edge + aa, pos); } float half_dash = dash_len * 0.5f; float center = half_dash; float d = abs(pos - center); return smoothstep(half_dash + aa, half_dash - aa, d); } // -------- Fragment -------- fragment float4 fragment_main(VertexOut in [[stage_in]], constant ShapeParams& params [[buffer(0)]], texture2d fill_texture [[texture(0)]], sampler smp [[sampler(0)]]) { float2 uv = in.uv; float2 p = (uv - 0.5f) * params.size; float2 half_size = params.size * 0.5f; float d = compute_sdf(p, half_size, params.shape_type, params.radius); float aa = max(params.feather, fwidth(d)); float fill_alpha = 0.0f; float stroke_alpha = 0.0f; if (params.stroke_thickness > 0.0f) { float inner_offset, outer_offset; if (params.stroke_align < 0.25f) { inner_offset = -params.stroke_thickness; outer_offset = 0.0f; } else if (params.stroke_align > 0.75f) { inner_offset = 0.0f; outer_offset = params.stroke_thickness; } else { inner_offset = -params.stroke_thickness * 0.5f; outer_offset = params.stroke_thickness * 0.5f; } float fill_d = d - inner_offset; fill_alpha = 1.0f - smoothstep(-aa, aa, fill_d); float start = smoothstep(-aa, aa, d - inner_offset); float end = 1.0f - smoothstep(-aa, aa, d - outer_offset); float in_stroke = start * end; float perim = perimeter_total(half_size, params.shape_type, params.radius); float dist_along = perimeter_dist_along(p, half_size, params.shape_type, params.radius); float dash = dash_mask_perimeter(dist_along, perim, params.dash_len, params.gap_len, params.dash_offset, params.cap_type, aa); stroke_alpha = in_stroke * dash; } else { fill_alpha = 1.0f - smoothstep(-aa, aa, d); } float4 fill_col = params.fill_color; if (params.has_texture > 0) { float2 tex_uv = uv; tex_uv = tex_uv * params.uv_transform.xy + params.uv_transform.zw; if (abs(params.uv_rotate) > 0.001f) { float2 center = float2(0.5f, 0.5f); tex_uv -= center; float c = cos(params.uv_rotate); float s = sin(params.uv_rotate); tex_uv = float2(tex_uv.x * c - tex_uv.y * s, tex_uv.x * s + tex_uv.y * c); tex_uv += center; } float4 tex_col = fill_texture.sample(smp, tex_uv); fill_col *= tex_col; } float4 fill_out = fill_col; fill_out.a *= fill_alpha; float4 stroke_out = params.stroke_color; stroke_out.a *= stroke_alpha; float sa = stroke_out.a; float fa = fill_out.a; float3 rgb_premul = stroke_out.rgb * sa + fill_out.rgb * fa * (1.0f - sa); float a = sa + fa * (1.0f - sa); float4 result = (a > 1e-6f) ? float4(rgb_premul / a, a) : float4(0.0f); result *= in.color; result.a *= params.opacity; return result; }