@@ -32,14 +32,29 @@ function toRadians(degrees: number): number {
3232 *
3333 * @param cornerRadius - Corner radius in pixels
3434 * @param cornerSmoothing - Smoothing factor 0-1 (0.6 = iOS squircle)
35+ * @param roundingAndSmoothingBudget - Maximum space available for the corner path
36+ * @param preserveSmoothing - If true, maintain smoothing aesthetic by adjusting bezier handles instead of reducing smoothing
3537 * @returns Path parameters for drawing the corner
3638 */
3739export function getPathParamsForCorner (
3840 cornerRadius : number ,
39- cornerSmoothing : number
41+ cornerSmoothing : number ,
42+ roundingAndSmoothingBudget : number = Infinity ,
43+ preserveSmoothing : boolean = true
4044) : CornerPathParams {
4145 // Total corner path length
42- const p = ( 1 + cornerSmoothing ) * cornerRadius ;
46+ let p = ( 1 + cornerSmoothing ) * cornerRadius ;
47+
48+ // When there's not enough space (p > budget), we have two options:
49+ // 1. preserveSmoothing = false (Figma's default): reduce smoothing to fit budget
50+ // This changes the arcMeasure angle, making corners look like circular arcs
51+ // 2. preserveSmoothing = true: keep original smoothing, adjust bezier handles
52+ // This maintains the squircle aesthetic even when space is limited
53+ if ( ! preserveSmoothing && p > roundingAndSmoothingBudget ) {
54+ const maxCornerSmoothing = roundingAndSmoothingBudget / cornerRadius - 1 ;
55+ cornerSmoothing = Math . max ( 0 , Math . min ( cornerSmoothing , maxCornerSmoothing ) ) ;
56+ p = Math . min ( p , roundingAndSmoothingBudget ) ;
57+ }
4358
4459 // Arc angle in degrees (90° when smoothing=0, 0° when smoothing=1)
4560 const arcMeasure = 90 * ( 1 - cornerSmoothing ) ;
@@ -63,8 +78,36 @@ export function getPathParamsForCorner(
6378
6479 // Bezier handle lengths
6580 // The remaining length after arc and transitions is split into 3 parts (1/3 for b, 2/3 for a)
66- const b = ( p - arcSectionLength - c - d ) / 3 ;
67- const a = 2 * b ;
81+ let b = ( p - arcSectionLength - c - d ) / 3 ;
82+ let a = 2 * b ;
83+
84+ // Adjust bezier handles if preserveSmoothing is enabled and we exceed the budget
85+ // This maintains the squircle S-curve characteristic while fitting within space constraints
86+ if ( preserveSmoothing && p > roundingAndSmoothingBudget ) {
87+ // Calculate how much we need to scale down
88+ const scaleFactor = roundingAndSmoothingBudget / p ;
89+
90+ // Proportionally scale all components to maintain smooth curve character
91+ // This keeps the relative proportions between a, b, c, d the same
92+ // This approach maintains the smooth S-curve without kinks
93+ const scaledA = a * scaleFactor ;
94+ const scaledB = b * scaleFactor ;
95+ const scaledC = c * scaleFactor ;
96+ const scaledD = d * scaleFactor ;
97+ const scaledArcLength = arcSectionLength * scaleFactor ;
98+
99+ // Use proportional scaling - this maintains smooth curves
100+ // by keeping the same relative proportions between all parameters
101+ return {
102+ a : scaledA ,
103+ b : scaledB ,
104+ c : scaledC ,
105+ d : scaledD ,
106+ p : roundingAndSmoothingBudget ,
107+ arcSectionLength : scaledArcLength ,
108+ cornerRadius,
109+ } ;
110+ }
68111
69112 return {
70113 a,
@@ -95,11 +138,15 @@ export function drawTopRightCorner(params: CornerPathParams): string {
95138 }
96139
97140 // First bezier curve (horizontal to arc transition)
98- // Then arc
141+ // Then arc (skip if degenerate to avoid rendering artifacts)
99142 // Then second bezier curve (arc to vertical transition)
143+ const arcCommand = arcSectionLength > 0.01
144+ ? `a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( arcSectionLength ) } ${ round ( arcSectionLength ) } `
145+ : '' ;
146+
100147 return `
101148 c ${ round ( a ) } 0 ${ round ( a + b ) } 0 ${ round ( a + b + c ) } ${ round ( d ) }
102- a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( arcSectionLength ) } ${ round ( arcSectionLength ) }
149+ ${ arcCommand }
103150 c ${ round ( d ) } ${ round ( c ) } ${ round ( d ) } ${ round ( b + c ) } ${ round ( d ) } ${ round ( a + b + c ) }
104151 ` . trim ( ) . replace ( / \s + / g, ' ' ) ;
105152}
@@ -114,9 +161,13 @@ export function drawBottomRightCorner(params: CornerPathParams): string {
114161 return `l 0 ${ round ( params . p ) } ` ;
115162 }
116163
164+ const arcCommand = arcSectionLength > 0.01
165+ ? `a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( - arcSectionLength ) } ${ round ( arcSectionLength ) } `
166+ : '' ;
167+
117168 return `
118169 c 0 ${ round ( a ) } 0 ${ round ( a + b ) } ${ round ( - d ) } ${ round ( a + b + c ) }
119- a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( - arcSectionLength ) } ${ round ( arcSectionLength ) }
170+ ${ arcCommand }
120171 c ${ round ( - c ) } ${ round ( d ) } ${ round ( - b - c ) } ${ round ( d ) } ${ round ( - a - b - c ) } ${ round ( d ) }
121172 ` . trim ( ) . replace ( / \s + / g, ' ' ) ;
122173}
@@ -131,9 +182,13 @@ export function drawBottomLeftCorner(params: CornerPathParams): string {
131182 return `l ${ round ( - params . p ) } 0` ;
132183 }
133184
185+ const arcCommand = arcSectionLength > 0.01
186+ ? `a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( - arcSectionLength ) } ${ round ( - arcSectionLength ) } `
187+ : '' ;
188+
134189 return `
135190 c ${ round ( - a ) } 0 ${ round ( - a - b ) } 0 ${ round ( - a - b - c ) } ${ round ( - d ) }
136- a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( - arcSectionLength ) } ${ round ( - arcSectionLength ) }
191+ ${ arcCommand }
137192 c ${ round ( - d ) } ${ round ( - c ) } ${ round ( - d ) } ${ round ( - b - c ) } ${ round ( - d ) } ${ round ( - a - b - c ) }
138193 ` . trim ( ) . replace ( / \s + / g, ' ' ) ;
139194}
@@ -148,9 +203,13 @@ export function drawTopLeftCorner(params: CornerPathParams): string {
148203 return `l 0 ${ round ( - params . p ) } ` ;
149204 }
150205
206+ const arcCommand = arcSectionLength > 0.01
207+ ? `a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( arcSectionLength ) } ${ round ( - arcSectionLength ) } `
208+ : '' ;
209+
151210 return `
152211 c 0 ${ round ( - a ) } 0 ${ round ( - a - b ) } ${ round ( d ) } ${ round ( - a - b - c ) }
153- a ${ round ( cornerRadius ) } ${ round ( cornerRadius ) } 0 0 1 ${ round ( arcSectionLength ) } ${ round ( - arcSectionLength ) }
212+ ${ arcCommand }
154213 c ${ round ( c ) } ${ round ( - d ) } ${ round ( b + c ) } ${ round ( - d ) } ${ round ( a + b + c ) } ${ round ( - d ) }
155214 ` . trim ( ) . replace ( / \s + / g, ' ' ) ;
156215}
@@ -176,11 +235,18 @@ export function generateFigmaSquirclePath(
176235 // Clamp smoothing to valid range
177236 const clampedSmoothing = Math . max ( 0 , Math . min ( 1 , smoothing ) ) ;
178237
179- // Calculate path parameters for all corners (assume uniform corners for now)
180- const topLeftParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing ) ;
181- const topRightParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing ) ;
182- const bottomRightParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing ) ;
183- const bottomLeftParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing ) ;
238+ // Calculate the budget for rounding and smoothing
239+ // Each corner's path extends along the edge, so budget is half the edge length
240+ const horizontalBudget = width / 2 ;
241+ const verticalBudget = height / 2 ;
242+
243+ // Calculate path parameters for all corners with budget constraints
244+ // Top-left and top-right share the top edge, so they compete for width/2
245+ // Left and right edges have height/2 budget
246+ const topLeftParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing , Math . min ( horizontalBudget , verticalBudget ) ) ;
247+ const topRightParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing , Math . min ( horizontalBudget , verticalBudget ) ) ;
248+ const bottomRightParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing , Math . min ( horizontalBudget , verticalBudget ) ) ;
249+ const bottomLeftParams = getPathParamsForCorner ( clampedRadius , clampedSmoothing , Math . min ( horizontalBudget , verticalBudget ) ) ;
184250
185251 // Build the complete path
186252 // Start from top-right corner, move counter-clockwise
0 commit comments