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.
- package/LICENSE +20 -0
- package/README.md +82 -2
- package/SquircleView.podspec +22 -0
- package/android/CMakeLists.txt +17 -0
- package/android/build.gradle +78 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/SquircleGeometryJni.cpp +52 -0
- package/android/src/main/java/com/squircleview/SquircleViewManager.kt +72 -0
- package/android/src/main/java/com/squircleview/SquircleViewPackage.kt +16 -0
- package/android/src/main/java/com/squircleview/geometry/SquircleGeometry.kt +58 -0
- package/android/src/main/java/com/squircleview/style/SquircleStyle.kt +142 -0
- package/android/src/main/java/com/squircleview/views/SquircleView.kt +361 -0
- package/common/cpp/SquircleGeometry.cpp +248 -0
- package/common/cpp/SquircleGeometry.h +75 -0
- package/ios/Geometry/SquirclePath.h +21 -0
- package/ios/Geometry/SquirclePath.mm +39 -0
- package/ios/Layers/SquircleBorderLayer.h +24 -0
- package/ios/Layers/SquircleBorderLayer.m +102 -0
- package/ios/Views/SquircleView.h +16 -0
- package/ios/Views/SquircleView.mm +337 -0
- package/lib/module/components/index.js +4 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/components/squircle-view.js +22 -0
- package/lib/module/components/squircle-view.js.map +1 -0
- package/lib/module/components/squircle-view.native.js +23 -0
- package/lib/module/components/squircle-view.native.js.map +1 -0
- package/lib/module/constants/index.js +5 -0
- package/lib/module/constants/index.js.map +1 -0
- package/lib/module/constants/native.constants.js +10 -0
- package/lib/module/constants/native.constants.js.map +1 -0
- package/lib/module/constants/squircle.constants.js +5 -0
- package/lib/module/constants/squircle.constants.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/interfaces/index.js +5 -0
- package/lib/module/interfaces/index.js.map +1 -0
- package/lib/module/interfaces/squircle-style.interface.js +4 -0
- package/lib/module/interfaces/squircle-style.interface.js.map +1 -0
- package/lib/module/interfaces/squircle-view.interface.js +4 -0
- package/lib/module/interfaces/squircle-view.interface.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/root/index.js +4 -0
- package/lib/module/root/index.js.map +1 -0
- package/lib/module/types/index.js +5 -0
- package/lib/module/types/index.js.map +1 -0
- package/lib/module/types/native-component-registry.d.js +2 -0
- package/lib/module/types/native-component-registry.d.js.map +1 -0
- package/lib/module/types/native.types.js +4 -0
- package/lib/module/types/native.types.js.map +1 -0
- package/lib/module/types/squircle.types.js +4 -0
- package/lib/module/types/squircle.types.js.map +1 -0
- package/lib/module/utils/index.js +4 -0
- package/lib/module/utils/index.js.map +1 -0
- package/lib/module/utils/style.utils.js +22 -0
- package/lib/module/utils/style.utils.js.map +1 -0
- package/lib/module/views/SquircleViewNativeComponent.ts +12 -0
- package/lib/module/views/index.js +15 -0
- package/lib/module/views/index.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/index.d.ts +2 -0
- package/lib/typescript/src/components/index.d.ts.map +1 -0
- package/lib/typescript/src/components/squircle-view.d.ts +5 -0
- package/lib/typescript/src/components/squircle-view.d.ts.map +1 -0
- package/lib/typescript/src/components/squircle-view.native.d.ts +5 -0
- package/lib/typescript/src/components/squircle-view.native.d.ts.map +1 -0
- package/lib/typescript/src/constants/index.d.ts +3 -0
- package/lib/typescript/src/constants/index.d.ts.map +1 -0
- package/lib/typescript/src/constants/native.constants.d.ts +8 -0
- package/lib/typescript/src/constants/native.constants.d.ts.map +1 -0
- package/lib/typescript/src/constants/squircle.constants.d.ts +3 -0
- package/lib/typescript/src/constants/squircle.constants.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/interfaces/index.d.ts +3 -0
- package/lib/typescript/src/interfaces/index.d.ts.map +1 -0
- package/lib/typescript/src/interfaces/squircle-style.interface.d.ts +7 -0
- package/lib/typescript/src/interfaces/squircle-style.interface.d.ts.map +1 -0
- package/lib/typescript/src/interfaces/squircle-view.interface.d.ts +10 -0
- package/lib/typescript/src/interfaces/squircle-view.interface.d.ts.map +1 -0
- package/lib/typescript/src/root/index.d.ts +6 -0
- package/lib/typescript/src/root/index.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +3 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -0
- package/lib/typescript/src/types/native.types.d.ts +5 -0
- package/lib/typescript/src/types/native.types.d.ts.map +1 -0
- package/lib/typescript/src/types/squircle.types.d.ts +6 -0
- package/lib/typescript/src/types/squircle.types.d.ts.map +1 -0
- package/lib/typescript/src/utils/index.d.ts +2 -0
- package/lib/typescript/src/utils/index.d.ts.map +1 -0
- package/lib/typescript/src/utils/style.utils.d.ts +6 -0
- package/lib/typescript/src/utils/style.utils.d.ts.map +1 -0
- package/lib/typescript/src/views/SquircleViewNativeComponent.d.ts +8 -0
- package/lib/typescript/src/views/SquircleViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/views/index.d.ts +7 -0
- package/lib/typescript/src/views/index.d.ts.map +1 -0
- package/package.json +200 -4
- package/src/components/index.ts +1 -0
- package/src/components/squircle-view.native.tsx +34 -0
- package/src/components/squircle-view.tsx +24 -0
- package/src/constants/index.ts +2 -0
- package/src/constants/native.constants.ts +9 -0
- package/src/constants/squircle.constants.ts +3 -0
- package/src/index.ts +1 -0
- package/src/interfaces/index.ts +2 -0
- package/src/interfaces/squircle-style.interface.ts +8 -0
- package/src/interfaces/squircle-view.interface.ts +11 -0
- package/src/root/index.ts +5 -0
- package/src/types/index.ts +2 -0
- package/src/types/native-component-registry.d.ts +15 -0
- package/src/types/native.types.ts +8 -0
- package/src/types/squircle.types.ts +7 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/style.utils.ts +17 -0
- package/src/views/SquircleViewNativeComponent.ts +12 -0
- 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);
|