fix(ios): honor corner-shape: squircle for non-uniform border radii - #11451
aleclarson wants to merge 1 commit into
Conversation
corner-shape: squircle maps to CALayer.cornerCurve for views with a uniform border-radius, but any view with non-uniform radii (e.g. a bottom sheet with rounded top corners) takes the custom CGPath generators, which hardcoded circular arcs and ignored cornerShape. This adds a squircle corner curve — a sampled n=4 superellipse, the curve the CSS spec defines for corner-shape: squircle and the closest parametric approximation of Apple's continuous corner — to the non-uniform outer clip path, the inner border clip path, and the box shadow paths. Round corners are unchanged. Corner masks are rasterized once per bounds change, so a 12-step polyline per quarter arc is visually indistinguishable at screen scale and keeps the code dependency-free (CAShapeLayer has no cornerCurve equivalent for custom paths).
|
View your CI Pipeline Execution ↗ for commit dd4717e
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
commit: |
|
Thanks for taking this on, and apologies up front: the confusion here is mine, not yours. When I added
Your helper draws the correct CSS Plan for 9.2 (details and numbers in #11452): fix the naming instead of building on the wrong mapping.
So I'd like to defer this PR until that lands and then take it as the non-uniform half of the spec Two cubics per corner. Fitted to the n=4 superellipse: max deviation 1.65e-4 r versus 1.9e-3 r for the 12-segment polyline, 2 path elements per corner instead of 12, exact tangents at both ends, and a smooth seam on the diagonal. Same signature as your helper, so no call site changes. Expressing points as // CSS corner-shape: squircle == superellipse(2), i.e. |x|^4 + |y|^4 = r^4.
// Each quarter is two cubic Béziers (one per half-corner, mirrored across the
// diagonal), fitted to within 1.7e-4·r of the exact curve.
const SQUIRCLE_H = 0.840896; // 0.5^(1/4): where the curve crosses the diagonal
const SQUIRCLE_A = 0.38905; // first control point, along the incoming edge
const SQUIRCLE_B = 0.1678; // second control point, offset along the diagonal tangent
function addSquircleArcToPoint(path: any, t1x: number, t1y: number, t2x: number, t2y: number, vx: number, vy: number): void {
CGPathAddLineToPoint(path, null, t1x, t1y);
// The corner-box corner opposite the vertex is the curve's center.
const cx = t1x + t2x - vx;
const cy = t1y + t2y - vy;
// d1: center → t1 (length = radius along the incoming edge), d2: center → t2.
const d1x = t1x - cx, d1y = t1y - cy;
const d2x = t2x - cx, d2y = t2y - cy;
const px = (X: number, Y: number) => cx + X * d2x + Y * d1x;
const py = (X: number, Y: number) => cy + X * d2y + Y * d1y;
const h = SQUIRCLE_H, a = SQUIRCLE_A, b = SQUIRCLE_B;
// (0,1) = t1 → (h,h) on the diagonal
CGPathAddCurveToPoint(path, null, px(a, 1), py(a, 1), px(h - b, h + b), py(h - b, h + b), px(h, h), py(h, h));
// (h,h) → (1,0) = t2, the first half mirrored across the diagonal
CGPathAddCurveToPoint(path, null, px(h + b, h - b), py(h + b, h - b), px(1, a), py(1, a), t2x, t2y);
}The radius capping you have is already right for this shape: unlike continuous, the superellipse consumes exactly r of each edge. And only Sorry again for the detour. Tracking the naming fix in #11452. |
PR Checklist
What is the current behavior?
#11365 added
corner-shape: squirclesupport by mapping it toCALayer.cornerCurve— but only on the uniformborder-radiuspath, where the view's owncornerRadiusdoes the rounding. Views with non-uniform radii (e.g.border-radius: 16px 16px 0 0, a bottom sheet with rounded top corners) are instead clipped by aCAShapeLayermask built from customCGPathgenerators — and those generators hardcodeCGPathAddArcToPoint/CGPathAddArc, socorner-shapeis silently ignored.What is the new behavior?
The non-uniform path generators honor
background.cornerShape. A newaddSquircleArcToPointhelper draws each corner as a sampled n=4 superellipse (the curve the CSS spec defines forcorner-shape: squircle, and the closest parametric approximation of Apple's continuous corner —CAShapeLayerhas nocornerCurveequivalent for custom paths). It is wired into:generateNonUniformBorderOuterClipPath(view mask,LayerMask.BORDER)generateNonUniformBorderInnerClipPath(uniform-color border fill)generateShadowLayerPaths(box-shadow inner/mask + shadow paths, both uniform and non-uniform radius cases)generateNonUniformBorderOuterClipRoundedPath(bounds-animation path updates)cornerShape: roundoutput is byte-for-byte what it was before — the circular-arc calls remain untouched behindif (squircle)branches.Implementation note: each quarter arc is emitted as a 12-segment polyline (
SQUIRCLE_CORNER_STEPS). Corner masks are rasterized once per bounds change, so the sampling is visually indistinguishable at screen scale while keeping the code dependency-free. Happy to switch to cubic Bézier segments if reviewers prefer — the polyline keeps the math readable but a two-curve fit would shrink the element count.Testing
The unit suite can't exercise real
CGPath/CAShapeLayercode, so I verified on an iOS 26.5 simulator:corner-shape: squircle→layer.cornerCurve === 'continuous'(unchanged behavior).corner-shape: squircle→layer.maskis aCAShapeLayerwhose pathcontainsPointa coordinate that lies inside a squircle but outside a circular arc of the same radius — confirming the mask is the superellipse, not a circle. A near-vertex control point correctly reports outside.corner-shapeunset/round → identical mask path shape as before.Refs #11365.