squircle-view 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +82 -2
  3. package/SquircleView.podspec +22 -0
  4. package/android/CMakeLists.txt +17 -0
  5. package/android/build.gradle +78 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/SquircleGeometryJni.cpp +52 -0
  8. package/android/src/main/java/com/squircleview/SquircleViewManager.kt +72 -0
  9. package/android/src/main/java/com/squircleview/SquircleViewPackage.kt +16 -0
  10. package/android/src/main/java/com/squircleview/geometry/SquircleGeometry.kt +58 -0
  11. package/android/src/main/java/com/squircleview/style/SquircleStyle.kt +142 -0
  12. package/android/src/main/java/com/squircleview/views/SquircleView.kt +361 -0
  13. package/common/cpp/SquircleGeometry.cpp +248 -0
  14. package/common/cpp/SquircleGeometry.h +75 -0
  15. package/ios/Geometry/SquirclePath.h +21 -0
  16. package/ios/Geometry/SquirclePath.mm +39 -0
  17. package/ios/Layers/SquircleBorderLayer.h +24 -0
  18. package/ios/Layers/SquircleBorderLayer.m +102 -0
  19. package/ios/Views/SquircleView.h +16 -0
  20. package/ios/Views/SquircleView.mm +337 -0
  21. package/lib/module/components/index.js +4 -0
  22. package/lib/module/components/index.js.map +1 -0
  23. package/lib/module/components/squircle-view.js +22 -0
  24. package/lib/module/components/squircle-view.js.map +1 -0
  25. package/lib/module/components/squircle-view.native.js +23 -0
  26. package/lib/module/components/squircle-view.native.js.map +1 -0
  27. package/lib/module/constants/index.js +5 -0
  28. package/lib/module/constants/index.js.map +1 -0
  29. package/lib/module/constants/native.constants.js +10 -0
  30. package/lib/module/constants/native.constants.js.map +1 -0
  31. package/lib/module/constants/squircle.constants.js +5 -0
  32. package/lib/module/constants/squircle.constants.js.map +1 -0
  33. package/lib/module/index.js +4 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/interfaces/index.js +5 -0
  36. package/lib/module/interfaces/index.js.map +1 -0
  37. package/lib/module/interfaces/squircle-style.interface.js +4 -0
  38. package/lib/module/interfaces/squircle-style.interface.js.map +1 -0
  39. package/lib/module/interfaces/squircle-view.interface.js +4 -0
  40. package/lib/module/interfaces/squircle-view.interface.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/root/index.js +4 -0
  43. package/lib/module/root/index.js.map +1 -0
  44. package/lib/module/types/index.js +5 -0
  45. package/lib/module/types/index.js.map +1 -0
  46. package/lib/module/types/native-component-registry.d.js +2 -0
  47. package/lib/module/types/native-component-registry.d.js.map +1 -0
  48. package/lib/module/types/native.types.js +4 -0
  49. package/lib/module/types/native.types.js.map +1 -0
  50. package/lib/module/types/squircle.types.js +4 -0
  51. package/lib/module/types/squircle.types.js.map +1 -0
  52. package/lib/module/utils/index.js +4 -0
  53. package/lib/module/utils/index.js.map +1 -0
  54. package/lib/module/utils/style.utils.js +22 -0
  55. package/lib/module/utils/style.utils.js.map +1 -0
  56. package/lib/module/views/SquircleViewNativeComponent.ts +12 -0
  57. package/lib/module/views/index.js +15 -0
  58. package/lib/module/views/index.js.map +1 -0
  59. package/lib/typescript/package.json +1 -0
  60. package/lib/typescript/src/components/index.d.ts +2 -0
  61. package/lib/typescript/src/components/index.d.ts.map +1 -0
  62. package/lib/typescript/src/components/squircle-view.d.ts +5 -0
  63. package/lib/typescript/src/components/squircle-view.d.ts.map +1 -0
  64. package/lib/typescript/src/components/squircle-view.native.d.ts +5 -0
  65. package/lib/typescript/src/components/squircle-view.native.d.ts.map +1 -0
  66. package/lib/typescript/src/constants/index.d.ts +3 -0
  67. package/lib/typescript/src/constants/index.d.ts.map +1 -0
  68. package/lib/typescript/src/constants/native.constants.d.ts +8 -0
  69. package/lib/typescript/src/constants/native.constants.d.ts.map +1 -0
  70. package/lib/typescript/src/constants/squircle.constants.d.ts +3 -0
  71. package/lib/typescript/src/constants/squircle.constants.d.ts.map +1 -0
  72. package/lib/typescript/src/index.d.ts +2 -0
  73. package/lib/typescript/src/index.d.ts.map +1 -0
  74. package/lib/typescript/src/interfaces/index.d.ts +3 -0
  75. package/lib/typescript/src/interfaces/index.d.ts.map +1 -0
  76. package/lib/typescript/src/interfaces/squircle-style.interface.d.ts +7 -0
  77. package/lib/typescript/src/interfaces/squircle-style.interface.d.ts.map +1 -0
  78. package/lib/typescript/src/interfaces/squircle-view.interface.d.ts +10 -0
  79. package/lib/typescript/src/interfaces/squircle-view.interface.d.ts.map +1 -0
  80. package/lib/typescript/src/root/index.d.ts +6 -0
  81. package/lib/typescript/src/root/index.d.ts.map +1 -0
  82. package/lib/typescript/src/types/index.d.ts +3 -0
  83. package/lib/typescript/src/types/index.d.ts.map +1 -0
  84. package/lib/typescript/src/types/native.types.d.ts +5 -0
  85. package/lib/typescript/src/types/native.types.d.ts.map +1 -0
  86. package/lib/typescript/src/types/squircle.types.d.ts +6 -0
  87. package/lib/typescript/src/types/squircle.types.d.ts.map +1 -0
  88. package/lib/typescript/src/utils/index.d.ts +2 -0
  89. package/lib/typescript/src/utils/index.d.ts.map +1 -0
  90. package/lib/typescript/src/utils/style.utils.d.ts +6 -0
  91. package/lib/typescript/src/utils/style.utils.d.ts.map +1 -0
  92. package/lib/typescript/src/views/SquircleViewNativeComponent.d.ts +8 -0
  93. package/lib/typescript/src/views/SquircleViewNativeComponent.d.ts.map +1 -0
  94. package/lib/typescript/src/views/index.d.ts +7 -0
  95. package/lib/typescript/src/views/index.d.ts.map +1 -0
  96. package/package.json +200 -4
  97. package/src/components/index.ts +1 -0
  98. package/src/components/squircle-view.native.tsx +34 -0
  99. package/src/components/squircle-view.tsx +24 -0
  100. package/src/constants/index.ts +2 -0
  101. package/src/constants/native.constants.ts +9 -0
  102. package/src/constants/squircle.constants.ts +3 -0
  103. package/src/index.ts +1 -0
  104. package/src/interfaces/index.ts +2 -0
  105. package/src/interfaces/squircle-style.interface.ts +8 -0
  106. package/src/interfaces/squircle-view.interface.ts +11 -0
  107. package/src/root/index.ts +5 -0
  108. package/src/types/index.ts +2 -0
  109. package/src/types/native-component-registry.d.ts +15 -0
  110. package/src/types/native.types.ts +8 -0
  111. package/src/types/squircle.types.ts +7 -0
  112. package/src/utils/index.ts +1 -0
  113. package/src/utils/style.utils.ts +17 -0
  114. package/src/views/SquircleViewNativeComponent.ts +12 -0
  115. package/src/views/index.ts +27 -0
@@ -0,0 +1,361 @@
1
+ package com.squircleview.views
2
+
3
+ import android.content.Context
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.DashPathEffect
7
+ import android.graphics.Outline
8
+ import android.graphics.Paint
9
+ import android.graphics.Path
10
+ import android.os.Build
11
+ import android.view.View
12
+ import android.view.ViewOutlineProvider
13
+ import kotlin.math.abs
14
+ import androidx.annotation.ColorInt
15
+ import com.facebook.react.modules.i18nmanager.I18nUtil
16
+ import com.facebook.react.uimanager.BackgroundStyleApplicator
17
+ import com.facebook.react.uimanager.LengthPercentage
18
+ import com.facebook.react.uimanager.style.BorderRadiusProp
19
+ import com.facebook.react.uimanager.style.BorderStyle
20
+ import com.facebook.react.uimanager.style.LogicalEdge
21
+ import com.facebook.react.views.view.ReactViewGroup
22
+ import com.squircleview.geometry.SquircleGeometry
23
+ import com.squircleview.style.SquircleStyle
24
+
25
+ public class SquircleView(context: Context) : ReactViewGroup(context) {
26
+ private val style = SquircleStyle()
27
+ private var borderSmoothing = 0f
28
+ private var isSquircle = false
29
+
30
+ private val radii = FloatArray(8)
31
+ private val borderWidths = FloatArray(4)
32
+ private val halfBorderWidths = FloatArray(4)
33
+ private val borderColors = IntArray(4)
34
+ private val noInsets = FloatArray(4)
35
+ private val geometryKey = FloatArray(15)
36
+ private val spokes = Array(4) { FloatArray(2) }
37
+ private var hasGeometry = false
38
+ private var hasStrokePath = false
39
+
40
+ private val outerPath = Path()
41
+ private val innerPath = Path()
42
+ private val borderPath = Path().apply { fillType = Path.FillType.EVEN_ODD }
43
+ private val strokePath = Path()
44
+ private val wedgePath = Path()
45
+
46
+ private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
47
+ private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE }
48
+ private var dashKey = 0f
49
+
50
+ private val squircleOutlineProvider =
51
+ object : ViewOutlineProvider() {
52
+ override fun getOutline(view: View, outline: Outline) {
53
+ updateGeometry()
54
+ if (outerPath.isEmpty) {
55
+ outline.setRect(0, 0, view.width, view.height)
56
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
57
+ outline.setPath(outerPath)
58
+ } else {
59
+ @Suppress("DEPRECATION") outline.setConvexPath(outerPath)
60
+ }
61
+ }
62
+ }
63
+
64
+ public fun setBorderSmoothing(smoothing: Float) {
65
+ borderSmoothing = SquircleGeometry.clampSmoothing(smoothing)
66
+ refresh()
67
+ }
68
+
69
+ public fun setSquircleBorderRadius(prop: BorderRadiusProp, radius: LengthPercentage?) {
70
+ style.radii[prop.ordinal] = radius
71
+ BackgroundStyleApplicator.setBorderRadius(this, prop, radius)
72
+ refresh()
73
+ }
74
+
75
+ public fun setSquircleBorderWidth(edge: LogicalEdge, width: Float?) {
76
+ style.borderWidths[edge.ordinal] = width
77
+ if (!isSquircle) {
78
+ BackgroundStyleApplicator.setBorderWidth(this, edge, width)
79
+ }
80
+ invalidateSquircle()
81
+ }
82
+
83
+ public fun setSquircleBorderColor(edge: LogicalEdge, @ColorInt color: Int?) {
84
+ style.borderColors[edge.ordinal] = color
85
+ if (!isSquircle) {
86
+ BackgroundStyleApplicator.setBorderColor(this, edge, color)
87
+ }
88
+ invalidateSquircle()
89
+ }
90
+
91
+ public fun setSquircleBorderStyle(borderStyle: BorderStyle?) {
92
+ style.borderStyle = borderStyle
93
+ if (!isSquircle) {
94
+ BackgroundStyleApplicator.setBorderStyle(this, borderStyle)
95
+ }
96
+ invalidateSquircle()
97
+ }
98
+
99
+ public fun setSquircleBackgroundColor(@ColorInt color: Int) {
100
+ style.backgroundColor = color
101
+ if (!isSquircle) {
102
+ BackgroundStyleApplicator.setBackgroundColor(this, color)
103
+ }
104
+ invalidateSquircle()
105
+ }
106
+
107
+ public fun resetSquircle() {
108
+ style.reset()
109
+ borderSmoothing = 0f
110
+ isSquircle = false
111
+ hasGeometry = false
112
+ hasStrokePath = false
113
+ setWillNotDraw(true)
114
+ outlineProvider = ViewOutlineProvider.BACKGROUND
115
+ }
116
+
117
+ override fun draw(canvas: Canvas) {
118
+ if (isSquircle) {
119
+ updateGeometry()
120
+ if (Color.alpha(style.backgroundColor) > 0) {
121
+ fillPaint.color = style.backgroundColor
122
+ canvas.drawPath(outerPath, fillPaint)
123
+ }
124
+ }
125
+ super.draw(canvas)
126
+ }
127
+
128
+ override fun onDraw(canvas: Canvas) {
129
+ super.onDraw(canvas)
130
+ if (isSquircle) {
131
+ drawBorder(canvas)
132
+ }
133
+ }
134
+
135
+ override fun dispatchDraw(canvas: Canvas) {
136
+ if (!isSquircle || overflow == null || overflow == OVERFLOW_VISIBLE) {
137
+ super.dispatchDraw(canvas)
138
+ return
139
+ }
140
+ updateGeometry()
141
+ val saveCount = canvas.save()
142
+ canvas.clipPath(innerPath)
143
+ super.dispatchDraw(canvas)
144
+ canvas.restoreToCount(saveCount)
145
+ }
146
+
147
+ override fun onRtlPropertiesChanged(layoutDirection: Int) {
148
+ super.onRtlPropertiesChanged(layoutDirection)
149
+ invalidateSquircle()
150
+ }
151
+
152
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
153
+ super.onSizeChanged(w, h, oldw, oldh)
154
+ invalidateOutline()
155
+ }
156
+
157
+ private fun refresh() {
158
+ val squircle = borderSmoothing > 0f && style.hasRadius()
159
+ if (squircle != isSquircle) {
160
+ isSquircle = squircle
161
+ applyNativeStyle(enabled = !squircle)
162
+ setWillNotDraw(!squircle)
163
+ outlineProvider = if (squircle) squircleOutlineProvider else ViewOutlineProvider.BACKGROUND
164
+ }
165
+ invalidateSquircle()
166
+ }
167
+
168
+ private fun applyNativeStyle(enabled: Boolean) {
169
+ BackgroundStyleApplicator.setBackgroundColor(this, if (enabled) style.backgroundColor else null)
170
+ for (edge in LogicalEdge.entries) {
171
+ val width = style.borderWidths[edge.ordinal]
172
+ if (width != null) {
173
+ BackgroundStyleApplicator.setBorderWidth(this, edge, if (enabled) width else null)
174
+ }
175
+ val color = style.borderColors[edge.ordinal]
176
+ if (color != null) {
177
+ BackgroundStyleApplicator.setBorderColor(this, edge, if (enabled) color else null)
178
+ }
179
+ }
180
+ if (style.borderStyle != null) {
181
+ BackgroundStyleApplicator.setBorderStyle(this, if (enabled) style.borderStyle else null)
182
+ }
183
+ }
184
+
185
+ private fun invalidateSquircle() {
186
+ invalidate()
187
+ invalidateOutline()
188
+ }
189
+
190
+ private val isRtl: Boolean
191
+ get() = layoutDirection == View.LAYOUT_DIRECTION_RTL
192
+
193
+ private val swapLeftAndRight: Boolean
194
+ get() = I18nUtil.instance.doLeftAndRightSwapInRTL(context)
195
+
196
+ private fun updateGeometry() {
197
+ val width = width.toFloat()
198
+ val height = height.toFloat()
199
+ val isRtl = isRtl
200
+ val swapLeftAndRight = swapLeftAndRight
201
+ style.resolveRadii(isRtl, swapLeftAndRight, width, height, radii)
202
+ style.resolveBorderWidths(isRtl, swapLeftAndRight, borderWidths)
203
+
204
+ if (hasGeometry && keyMatches(width, height)) {
205
+ return
206
+ }
207
+ writeKey(width, height)
208
+ hasGeometry = true
209
+ hasStrokePath = false
210
+
211
+ outerPath.rewind()
212
+ innerPath.rewind()
213
+ borderPath.rewind()
214
+ SquircleGeometry.appendPath(outerPath, width, height, radii, noInsets, borderSmoothing)
215
+ SquircleGeometry.appendPath(innerPath, width, height, radii, borderWidths, borderSmoothing)
216
+ borderPath.addPath(outerPath)
217
+ borderPath.addPath(innerPath)
218
+ }
219
+
220
+ private fun keyMatches(width: Float, height: Float): Boolean {
221
+ if (geometryKey[0] != width || geometryKey[1] != height || geometryKey[2] != borderSmoothing) {
222
+ return false
223
+ }
224
+ for (index in radii.indices) {
225
+ if (geometryKey[3 + index] != radii[index]) return false
226
+ }
227
+ for (index in borderWidths.indices) {
228
+ if (geometryKey[11 + index] != borderWidths[index]) return false
229
+ }
230
+ return true
231
+ }
232
+
233
+ private fun writeKey(width: Float, height: Float) {
234
+ geometryKey[0] = width
235
+ geometryKey[1] = height
236
+ geometryKey[2] = borderSmoothing
237
+ radii.copyInto(geometryKey, 3)
238
+ borderWidths.copyInto(geometryKey, 11)
239
+ }
240
+
241
+ private fun strokePath(): Path {
242
+ if (!hasStrokePath) {
243
+ for (index in borderWidths.indices) {
244
+ halfBorderWidths[index] = borderWidths[index] / 2f
245
+ }
246
+ strokePath.rewind()
247
+ SquircleGeometry.appendPath(strokePath, width.toFloat(), height.toFloat(), radii, halfBorderWidths, borderSmoothing)
248
+ hasStrokePath = true
249
+ }
250
+ return strokePath
251
+ }
252
+
253
+ private fun drawBorder(canvas: Canvas) {
254
+ if (borderWidths.all { it <= 0f }) {
255
+ return
256
+ }
257
+ style.resolveBorderColors(isRtl, swapLeftAndRight, borderColors)
258
+ val color = borderColors[0]
259
+
260
+ if (borderColors.any { it != color }) {
261
+ drawMulticolorBorder(canvas)
262
+ return
263
+ }
264
+
265
+ val borderStyle = style.borderStyle
266
+ val strokeWidth = borderWidths[0]
267
+ if (borderStyle != null && borderStyle != BorderStyle.SOLID && borderWidths.all { it == strokeWidth }) {
268
+ val dotted = borderStyle == BorderStyle.DOTTED
269
+ strokePaint.color = color
270
+ strokePaint.strokeWidth = strokeWidth
271
+ strokePaint.strokeCap = if (dotted) Paint.Cap.ROUND else Paint.Cap.BUTT
272
+ val key = if (dotted) -strokeWidth else strokeWidth
273
+ if (dashKey != key) {
274
+ dashKey = key
275
+ strokePaint.pathEffect =
276
+ DashPathEffect(
277
+ if (dotted) floatArrayOf(0f, strokeWidth * 2) else floatArrayOf(strokeWidth * 3, strokeWidth * 3),
278
+ 0f,
279
+ )
280
+ }
281
+ canvas.drawPath(strokePath(), strokePaint)
282
+ return
283
+ }
284
+
285
+ fillPaint.color = color
286
+ canvas.drawPath(borderPath, fillPaint)
287
+ }
288
+
289
+ private fun drawMulticolorBorder(canvas: Canvas) {
290
+ val width = width.toFloat()
291
+ val height = height.toFloat()
292
+ val centerX = width / 2f
293
+ val centerY = height / 2f
294
+ val top = borderWidths[0]
295
+ val right = borderWidths[1]
296
+ val bottom = borderWidths[2]
297
+ val left = borderWidths[3]
298
+
299
+ val topLeft = spoke(spokes[0], 0f, 0f, left, top, centerX, centerY)
300
+ val topRight = spoke(spokes[1], width, 0f, -right, top, centerX, centerY)
301
+ val bottomRight = spoke(spokes[2], width, height, -right, -bottom, centerX, centerY)
302
+ val bottomLeft = spoke(spokes[3], 0f, height, left, -bottom, centerX, centerY)
303
+
304
+ val saveCount = canvas.save()
305
+ canvas.clipPath(borderPath)
306
+ drawWedge(canvas, borderColors[0], 0f, 0f, width, 0f, topRight, topLeft, centerX, centerY)
307
+ drawWedge(canvas, borderColors[1], width, 0f, width, height, bottomRight, topRight, centerX, centerY)
308
+ drawWedge(canvas, borderColors[2], width, height, 0f, height, bottomLeft, bottomRight, centerX, centerY)
309
+ drawWedge(canvas, borderColors[3], 0f, height, 0f, 0f, topLeft, bottomLeft, centerX, centerY)
310
+ canvas.restoreToCount(saveCount)
311
+ }
312
+
313
+ private fun spoke(
314
+ out: FloatArray,
315
+ x: Float,
316
+ y: Float,
317
+ dx: Float,
318
+ dy: Float,
319
+ halfWidth: Float,
320
+ halfHeight: Float,
321
+ ): FloatArray {
322
+ var scale = Float.MAX_VALUE
323
+ if (dx != 0f) scale = minOf(scale, halfWidth / abs(dx))
324
+ if (dy != 0f) scale = minOf(scale, halfHeight / abs(dy))
325
+ if (scale == Float.MAX_VALUE) {
326
+ out[0] = x
327
+ out[1] = y
328
+ } else {
329
+ out[0] = x + dx * scale
330
+ out[1] = y + dy * scale
331
+ }
332
+ return out
333
+ }
334
+
335
+ private fun drawWedge(
336
+ canvas: Canvas,
337
+ @ColorInt color: Int,
338
+ startX: Float,
339
+ startY: Float,
340
+ endX: Float,
341
+ endY: Float,
342
+ endSpoke: FloatArray,
343
+ startSpoke: FloatArray,
344
+ centerX: Float,
345
+ centerY: Float,
346
+ ) {
347
+ wedgePath.rewind()
348
+ wedgePath.moveTo(startX, startY)
349
+ wedgePath.lineTo(endX, endY)
350
+ wedgePath.lineTo(endSpoke[0], endSpoke[1])
351
+ wedgePath.lineTo(centerX, centerY)
352
+ wedgePath.lineTo(startSpoke[0], startSpoke[1])
353
+ wedgePath.close()
354
+ fillPaint.color = color
355
+ canvas.drawPath(wedgePath, fillPaint)
356
+ }
357
+
358
+ private companion object {
359
+ const val OVERFLOW_VISIBLE = "visible"
360
+ }
361
+ }
@@ -0,0 +1,248 @@
1
+ //
2
+ // SquircleGeometry.cpp
3
+ // Pods
4
+ //
5
+ // Created by rit3zh CX on 10/7/26.
6
+ //
7
+
8
+ #include "SquircleGeometry.h"
9
+
10
+ #include <algorithm>
11
+ #include <cmath>
12
+ #include <numbers>
13
+
14
+ namespace squircle {
15
+
16
+ namespace {
17
+
18
+ constexpr double kHalfPi = std::numbers::pi / 2;
19
+
20
+ struct AxisParams {
21
+ double radius = 0;
22
+ double a = 0;
23
+ double b = 0;
24
+ double c = 0;
25
+ double d = 0;
26
+ double arc = 0;
27
+ double p = 0;
28
+ };
29
+
30
+ struct CornerFrame {
31
+ Point origin;
32
+ Point in;
33
+ Point out;
34
+
35
+ Point map(double x, double y) const {
36
+ return {origin.x + x * in.x + y * out.x, origin.y + x * in.y + y * out.y};
37
+ }
38
+ };
39
+
40
+ struct Corner {
41
+ CornerFrame frame;
42
+ AxisParams in;
43
+ AxisParams out;
44
+ };
45
+
46
+ double shareOfSide(double radius, double adjacentRadius, double side) {
47
+ const double total = radius + adjacentRadius;
48
+ return total > 0 ? side * radius / total : 0;
49
+ }
50
+
51
+ AxisParams axisParams(double radius, double smoothing, double budget) {
52
+ radius = std::min(radius, budget);
53
+ if (radius <= 0) {
54
+ return {};
55
+ }
56
+
57
+ const double sweep = kHalfPi * (1 - smoothing);
58
+ const double angle = smoothing * kHalfPi / 2;
59
+ const double arc = std::sin(sweep / 2) * radius * std::numbers::sqrt2;
60
+ const double c = radius * std::tan(angle / 2) * std::cos(angle);
61
+ const double d = c * std::tan(angle);
62
+
63
+ double b = ((1 + smoothing) * radius - arc - c - d) / 3;
64
+ double a = 2 * b;
65
+
66
+ if ((1 + smoothing) * radius > budget) {
67
+ const double available = std::max(0.0, budget - arc - c - d);
68
+ b = std::min(b, available * 5 / 6);
69
+ a = available - b;
70
+ }
71
+
72
+ return {radius, a, b, c, d, arc, a + b + c + d + arc};
73
+ }
74
+
75
+ Point ellipsePoint(const AxisParams &in, const AxisParams &out, double theta) {
76
+ return {-in.radius + in.radius * std::cos(theta), out.radius + out.radius * std::sin(theta)};
77
+ }
78
+
79
+ Point ellipseTangent(const AxisParams &in, const AxisParams &out, double theta) {
80
+ return {-in.radius * std::sin(theta), out.radius * std::cos(theta)};
81
+ }
82
+
83
+ void appendCubic(Path &path, const CornerFrame &frame, Point c1, Point c2, Point end) {
84
+ path.push_back({Verb::Cubic, {frame.map(c1.x, c1.y), frame.map(c2.x, c2.y), frame.map(end.x, end.y)}});
85
+ }
86
+
87
+ void appendCorner(Path &path, const Corner &corner, double smoothing) {
88
+ const auto &frame = corner.frame;
89
+ const auto &in = corner.in;
90
+ const auto &out = corner.out;
91
+
92
+ path.push_back({Verb::Line, {frame.map(-in.p, 0)}});
93
+
94
+ if (in.radius <= 0) {
95
+ return;
96
+ }
97
+
98
+ if (smoothing > 0) {
99
+ appendCubic(
100
+ path,
101
+ frame,
102
+ {-in.p + in.a, 0},
103
+ {-in.p + in.a + in.b, 0},
104
+ {-(in.arc + in.d), out.d});
105
+ }
106
+
107
+ const double sweep = kHalfPi * (1 - smoothing);
108
+ if (sweep > 0) {
109
+ const double start = -kHalfPi + smoothing * kHalfPi / 2;
110
+ const double end = start + sweep;
111
+ const double k = 4.0 / 3.0 * std::tan(sweep / 4);
112
+ const Point from = ellipsePoint(in, out, start);
113
+ const Point to = ellipsePoint(in, out, end);
114
+ const Point fromTangent = ellipseTangent(in, out, start);
115
+ const Point toTangent = ellipseTangent(in, out, end);
116
+ appendCubic(
117
+ path,
118
+ frame,
119
+ {from.x + k * fromTangent.x, from.y + k * fromTangent.y},
120
+ {to.x - k * toTangent.x, to.y - k * toTangent.y},
121
+ to);
122
+ }
123
+
124
+ if (smoothing > 0) {
125
+ appendCubic(
126
+ path,
127
+ frame,
128
+ {0, out.arc + out.d + out.c},
129
+ {0, out.arc + out.d + out.c + out.b},
130
+ {0, out.p});
131
+ }
132
+ }
133
+
134
+ Corner makeCorner(
135
+ CornerFrame frame,
136
+ double inRadius,
137
+ double outRadius,
138
+ double inBudget,
139
+ double outBudget,
140
+ double smoothing) {
141
+ if (inRadius <= 0 || outRadius <= 0) {
142
+ return {frame, {}, {}};
143
+ }
144
+ return {frame, axisParams(inRadius, smoothing, inBudget), axisParams(outRadius, smoothing, outBudget)};
145
+ }
146
+
147
+ } // namespace
148
+
149
+ double clampSmoothing(double smoothing) {
150
+ if (!(smoothing > 0)) {
151
+ return 0;
152
+ }
153
+ return std::min(smoothing, 1.0);
154
+ }
155
+
156
+ bool hasRoundedCorner(const CornerRadii &radii) {
157
+ for (const auto &corner : {radii.topLeft, radii.topRight, radii.bottomRight, radii.bottomLeft}) {
158
+ if (corner.horizontal > 0 && corner.vertical > 0) {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ Path buildPath(const Rect &rect, const CornerRadii &radii, double smoothing) {
166
+ Path path;
167
+ if (!(rect.width > 0) || !(rect.height > 0)) {
168
+ return path;
169
+ }
170
+
171
+ smoothing = clampSmoothing(smoothing);
172
+
173
+ const double w = rect.width;
174
+ const double h = rect.height;
175
+ const double left = rect.x;
176
+ const double top = rect.y;
177
+ const double right = rect.x + w;
178
+ const double bottom = rect.y + h;
179
+
180
+ const auto &tl = radii.topLeft;
181
+ const auto &tr = radii.topRight;
182
+ const auto &br = radii.bottomRight;
183
+ const auto &bl = radii.bottomLeft;
184
+
185
+ const std::array<Corner, 4> corners = {
186
+ makeCorner(
187
+ {{right, top}, {1, 0}, {0, 1}},
188
+ tr.horizontal,
189
+ tr.vertical,
190
+ shareOfSide(tr.horizontal, tl.horizontal, w),
191
+ shareOfSide(tr.vertical, br.vertical, h),
192
+ smoothing),
193
+ makeCorner(
194
+ {{right, bottom}, {0, 1}, {-1, 0}},
195
+ br.vertical,
196
+ br.horizontal,
197
+ shareOfSide(br.vertical, tr.vertical, h),
198
+ shareOfSide(br.horizontal, bl.horizontal, w),
199
+ smoothing),
200
+ makeCorner(
201
+ {{left, bottom}, {-1, 0}, {0, -1}},
202
+ bl.horizontal,
203
+ bl.vertical,
204
+ shareOfSide(bl.horizontal, br.horizontal, w),
205
+ shareOfSide(bl.vertical, tl.vertical, h),
206
+ smoothing),
207
+ makeCorner(
208
+ {{left, top}, {0, -1}, {1, 0}},
209
+ tl.vertical,
210
+ tl.horizontal,
211
+ shareOfSide(tl.vertical, bl.vertical, h),
212
+ shareOfSide(tl.horizontal, tr.horizontal, w),
213
+ smoothing),
214
+ };
215
+
216
+ path.reserve(18);
217
+
218
+ const auto &last = corners.back();
219
+ path.push_back({Verb::Move, {last.frame.map(0, last.out.p)}});
220
+
221
+ for (const auto &corner : corners) {
222
+ appendCorner(path, corner, smoothing);
223
+ }
224
+
225
+ path.push_back({Verb::Close, {}});
226
+ return path;
227
+ }
228
+
229
+ Rect insetRect(const Rect &rect, const Insets &insets) {
230
+ return {
231
+ rect.x + insets.left,
232
+ rect.y + insets.top,
233
+ std::max(0.0, rect.width - insets.left - insets.right),
234
+ std::max(0.0, rect.height - insets.top - insets.bottom)};
235
+ }
236
+
237
+ CornerRadii insetRadii(const CornerRadii &radii, const Insets &insets) {
238
+ const auto inset = [](const CornerRadius &radius, double horizontal, double vertical) {
239
+ return CornerRadius{std::max(0.0, radius.horizontal - horizontal), std::max(0.0, radius.vertical - vertical)};
240
+ };
241
+ return {
242
+ inset(radii.topLeft, insets.left, insets.top),
243
+ inset(radii.topRight, insets.right, insets.top),
244
+ inset(radii.bottomRight, insets.right, insets.bottom),
245
+ inset(radii.bottomLeft, insets.left, insets.bottom)};
246
+ }
247
+
248
+ } // namespace squircle
@@ -0,0 +1,75 @@
1
+ //
2
+ // SquircleGeometry.h
3
+ // Pods
4
+ //
5
+ // Created by rit3zh CX on 10/7/26.
6
+ //
7
+
8
+ #pragma once
9
+
10
+ #include <array>
11
+ #include <cstdint>
12
+ #include <vector>
13
+
14
+ namespace squircle {
15
+
16
+ struct Point {
17
+ double x = 0;
18
+ double y = 0;
19
+
20
+ bool operator==(const Point &other) const = default;
21
+ };
22
+
23
+ struct Rect {
24
+ double x = 0;
25
+ double y = 0;
26
+ double width = 0;
27
+ double height = 0;
28
+
29
+ bool operator==(const Rect &other) const = default;
30
+ };
31
+
32
+ struct Insets {
33
+ double top = 0;
34
+ double right = 0;
35
+ double bottom = 0;
36
+ double left = 0;
37
+
38
+ bool operator==(const Insets &other) const = default;
39
+ };
40
+
41
+ struct CornerRadius {
42
+ double horizontal = 0;
43
+ double vertical = 0;
44
+
45
+ bool operator==(const CornerRadius &other) const = default;
46
+ };
47
+
48
+ struct CornerRadii {
49
+ CornerRadius topLeft;
50
+ CornerRadius topRight;
51
+ CornerRadius bottomRight;
52
+ CornerRadius bottomLeft;
53
+
54
+ bool operator==(const CornerRadii &other) const = default;
55
+ };
56
+
57
+ enum class Verb : uint8_t { Move, Line, Cubic, Close };
58
+
59
+ struct PathElement {
60
+ Verb verb;
61
+ std::array<Point, 3> points;
62
+ };
63
+
64
+ using Path = std::vector<PathElement>;
65
+
66
+ double clampSmoothing(double smoothing);
67
+ bool hasRoundedCorner(const CornerRadii &radii);
68
+
69
+ Path buildPath(const Rect &rect, const CornerRadii &radii, double smoothing);
70
+
71
+ Rect insetRect(const Rect &rect, const Insets &insets);
72
+
73
+ CornerRadii insetRadii(const CornerRadii &radii, const Insets &insets);
74
+
75
+ } // namespace squircle
@@ -0,0 +1,21 @@
1
+ //
2
+ // SquirclePath.h
3
+ // Pods
4
+ //
5
+ // Created by rit3zh CX on 10/7/26.
6
+ //
7
+
8
+ #import <CoreGraphics/CoreGraphics.h>
9
+
10
+ #import "SquircleGeometry.h"
11
+
12
+ CF_RETURNS_RETAINED CGPathRef _Nullable SquirclePathCreate(
13
+ const squircle::Rect &rect,
14
+ const squircle::CornerRadii &radii,
15
+ double smoothing);
16
+
17
+ void SquirclePathAppend(
18
+ CGMutablePathRef _Nonnull path,
19
+ const squircle::Rect &rect,
20
+ const squircle::CornerRadii &radii,
21
+ double smoothing);