Skip to content

corner-shape: squircle renders Apple continuous, not CSS superellipse(2): make corner-shape spec-correct and add -ios-corner-shape: continuous #11452

Description

@edusperoni

Summary

corner-shape: squircle (added in #11365) maps to kCACornerCurveContinuous on iOS. I wrote that mapping believing Apple's continuous corner and the CSS squircle are the same curve. They are not, and the difference is large. Proposal: make corner-shape follow the CSS Borders Level 4 definitions, and expose Apple's continuous corner through a separate iOS-only property, -ios-corner-shape. Target: 9.2.

What is wrong

Measured on the iOS simulator against a real CALayer with cornerCurve = .continuous (200×100 layer, r=20, 4× raster diff):

Shape at the same border-radius Max gap from Apple continuous Pixels differing
Circle (CSS round, kCACornerCurveCircular) 0.019 r (~0.3 pt at r=16) 756
CSS squircle = superellipse(2) 0.187 r (~3 pt at r=16) 4368

Apple's continuous corner is essentially CSS round with the curvature eased in over a 1.528665 r run-up (CALayer.cornerCurveExpansionFactor(.continuous)). The CSS squircle is a much squarer corner. No superellipse(K) at any radius gets closer to continuous than a plain circle does, so the two cannot be reconciled by picking a different K.

Consequences today:

Proposal

  1. corner-shape follows the spec.
    • round = kCACornerCurveCircular (unchanged; it is exactly superellipse(1)).
    • squircle = superellipse(2), drawn as a CAShapeLayer mask path on every branch, including the uniform-radius fast path, since Core Animation cannot draw it. Each corner is two cubic Béziers (one per half-corner, mirrored across the diagonal): in a unit corner box from (0, 1) to the diagonal point (h, h) with h = 0.5^(1/4) = 0.840896, control points (0.38905, 1) and (h − 0.1678, h + 0.1678). Max deviation from the exact curve is 1.65e-4 r. This is how Blink renders it, and superellipse(K) can be added later on the same path by refitting the three constants per K.
    • Android can implement the same path with Path.cubicTo.
  2. -ios-corner-shape: continuous (iOS-only property, ignored elsewhere) maps to kCACornerCurveContinuous on the uniform fast path, so it stays free. For non-uniform radii, borders and shadows it draws Apple's curve, which is three cubic Béziers per corner. With d1/d2 the edge vectors from the vertex (length r each), every point is v + a·d1 + b·d2:
    line  to (1.528665, 0)
    cubic cp (1.08849, 0)          cp (0.868407, 0)          to (0.631494, 0.074911)
    cubic cp (0.372824, 0.169060)  cp (0.169060, 0.372824)   to (0.074911, 0.631494)
    cubic cp (0, 0.868407)         cp (0, 1.08849)           to (0, 1.528665)
    
    This is byte-for-byte what UIBezierPath(roundedRect:byRoundingCorners:cornerRadii:) emits and renders pixel-identical to the layer. The corner consumes cornerCurveExpansionFactor(.continuous) × r of each edge, so the radius cap needs that factor. When the non-zero radii are equal, maskedCorners + cornerRadius + cornerCurve renders a subset of corners natively with no mask at all.
  3. If both are set, -ios-corner-shape wins on iOS (platform override), otherwise corner-shape applies.

Breaking change

corner-shape: squircle was released recently and currently renders continuous. After this change it renders the (squarer) spec shape and goes through a mask. Anyone who adopted it for the iOS system look migrates to -ios-corner-shape: continuous. Doing this early keeps the blast radius small; the changelog should call it out explicitly.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions